From 9657daeb4b81f940bc6289807c18997122d09c54 Mon Sep 17 00:00:00 2001 From: pixelpix13 Date: Fri, 24 Jul 2026 15:55:18 -0500 Subject: [PATCH] Add PR review team analytics dashboard for reviewer requirement rankings. Port Manvitha's review-summary chart onto latest development with dark mode, responsive layout, and PR Dashboard navigation. Co-authored-by: Cursor --- .../Analytics/AnalyticsDashboard.jsx | 54 ++++ .../Analytics/AnalyticsDashboard.module.css | 129 ++++++++++ src/components/Analytics/PopularPRChart.jsx | 66 +++++ .../Analytics/ReviewersRequirementChart.jsx | 239 ++++++++++++++++++ src/components/Header/Header.jsx | 10 +- src/routes.jsx | 22 ++ src/utils/URL.js | 10 + 7 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 src/components/Analytics/AnalyticsDashboard.jsx create mode 100644 src/components/Analytics/AnalyticsDashboard.module.css create mode 100644 src/components/Analytics/PopularPRChart.jsx create mode 100644 src/components/Analytics/ReviewersRequirementChart.jsx diff --git a/src/components/Analytics/AnalyticsDashboard.jsx b/src/components/Analytics/AnalyticsDashboard.jsx new file mode 100644 index 0000000000..abd1ca0d2e --- /dev/null +++ b/src/components/Analytics/AnalyticsDashboard.jsx @@ -0,0 +1,54 @@ +import { useState } from 'react'; +import { useSelector } from 'react-redux'; +import ReviewersRequirementChart from './ReviewersRequirementChart'; +import styles from './AnalyticsDashboard.module.css'; + +const durationOptions = [ + { label: 'Last Week', value: 'lastWeek' }, + { label: 'Last 2 weeks', value: 'last2weeks' }, + { label: 'Last Month', value: 'lastMonth' }, + { label: 'All Time', value: 'allTime' }, +]; + +const AnalyticsDashboard = () => { + const darkMode = useSelector(state => state.theme.darkMode); + const [duration, setDuration] = useState('lastWeek'); + + return ( +
+
+

Reviewers Ranked by Requirement Satisfied

+ +
+ + +
+
+ +

+ Shows how many PR reviews each reviewer completed in the selected period, grouped by review + quality. The list length comes from the backend for that duration (not a fixed frontend + limit). +

+ +
+ +
+
+ ); +}; + +export default AnalyticsDashboard; diff --git a/src/components/Analytics/AnalyticsDashboard.module.css b/src/components/Analytics/AnalyticsDashboard.module.css new file mode 100644 index 0000000000..2ff9f30254 --- /dev/null +++ b/src/components/Analytics/AnalyticsDashboard.module.css @@ -0,0 +1,129 @@ +.page { + width: 100%; + max-width: 1400px; + margin: 0 auto; + padding: 1.5rem 1.25rem 2.5rem; + box-sizing: border-box; + min-height: calc(100vh - 120px); +} + +.header { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.25rem; +} + +.title { + margin: 0; + font-size: clamp(1.25rem, 2.5vw, 1.75rem); + font-weight: 600; + line-height: 1.3; + color: #052c65; + flex: 1 1 280px; +} + +.filterGroup { + display: flex; + align-items: center; + gap: 0.5rem; + flex: 0 0 auto; +} + +.filterLabel { + margin: 0; + font-weight: 500; + color: #1f2937; + white-space: nowrap; +} + +.filterSelect { + min-width: 160px; + padding: 0.45rem 0.75rem; + border: 1px solid #d1d5db; + border-radius: 6px; + background: #fff; + color: #111827; + font-size: 0.95rem; +} + +.chartCard { + width: 100%; + border: 1px solid #e5e7eb; + border-radius: 10px; + background: #fff; + padding: 1rem 0.75rem 1.25rem; + box-sizing: border-box; + overflow-x: auto; +} + +.helperText { + margin: 0 0 1rem; + font-size: 0.9rem; + color: #4b5563; +} + +.statusMessage { + display: grid; + place-items: center; + min-height: 220px; + margin: 0; + text-align: center; + color: #374151; + font-size: 1rem; +} + +/* Dark mode */ +.pageDark { + color: #f8fafc; +} + +.pageDark .title { + color: #e2e8f0; +} + +.pageDark .filterLabel { + color: #e2e8f0; +} + +.pageDark .filterSelect { + background: #2d3748; + border-color: #4a5568; + color: #f8fafc; +} + +.pageDark .chartCard { + background: #1a202c; + border-color: #4a5568; +} + +.pageDark .helperText, +.pageDark .statusMessage { + color: #cbd5e1; +} + +@media (width <= 768px) { + .page { + padding: 1rem 0.75rem 2rem; + } + + .header { + flex-direction: column; + align-items: stretch; + } + + .filterGroup { + width: 100%; + } + + .filterSelect { + flex: 1; + min-width: 0; + } + + .chartCard { + padding: 0.75rem 0.25rem 1rem; + } +} diff --git a/src/components/Analytics/PopularPRChart.jsx b/src/components/Analytics/PopularPRChart.jsx new file mode 100644 index 0000000000..e037f3013d --- /dev/null +++ b/src/components/Analytics/PopularPRChart.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Bar, BarChart, LabelList, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; + +const data = [ + { prNumber: 'PR1234', reviews: 10 }, + { prNumber: 'PR1235', reviews: 8 }, + { prNumber: 'PR1236', reviews: 15 }, + { prNumber: 'PR1237', reviews: 5 }, + { prNumber: 'PR1238', reviews: 12 }, + { prNumber: 'PR1231', reviews: 12 }, + { prNumber: 'PR1256', reviews: 17 }, + { prNumber: 'PR1255', reviews: 19 }, + { prNumber: 'PR1254', reviews: 1 }, + { prNumber: 'PR1253', reviews: 2 }, + { prNumber: 'PR1252', reviews: 7 }, + { prNumber: 'PR1251', reviews: 9 }, + { prNumber: 'PR1250', reviews: 11 }, + { prNumber: 'PR1249', reviews: 19 }, + { prNumber: 'PR1248', reviews: 0 }, + { prNumber: 'PR1242', reviews: 2 }, + { prNumber: 'PR1241', reviews: 50 }, +]; + +const PopularPRChart = () => { + const filteredData = data; + + return ( +
+ + + + + + + + + + + + + +
+ ); +}; + +export default PopularPRChart; diff --git a/src/components/Analytics/ReviewersRequirementChart.jsx b/src/components/Analytics/ReviewersRequirementChart.jsx new file mode 100644 index 0000000000..af46a50a8e --- /dev/null +++ b/src/components/Analytics/ReviewersRequirementChart.jsx @@ -0,0 +1,239 @@ +import axios from 'axios'; +import { useEffect, useState } from 'react'; +import { + Bar, + BarChart, + CartesianGrid, + LabelList, + Legend, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { ENDPOINTS } from '../../utils/URL'; +import styles from './AnalyticsDashboard.module.css'; + +const useYAxisWidth = () => { + const [yAxisWidth, setYAxisWidth] = useState(180); + + useEffect(() => { + const updateWidth = () => { + setYAxisWidth(window.innerWidth < 768 ? 110 : 180); + }; + + updateWidth(); + window.addEventListener('resize', updateWidth); + return () => window.removeEventListener('resize', updateWidth); + }, []); + + return yAxisWidth; +}; + +const COLORS = { + exceptional: '#052C65', + sufficient: '#4682B4', + needsChanges: '#FF8C00', + didNotReview: '#A9A9A9', +}; + +const getCount = (counts, key) => counts?.[key] || 0; + +const normalizeReviewCounts = item => { + const counts = item.counts || {}; + const exceptional = getCount(counts, 'Exceptional'); + const sufficient = getCount(counts, 'Sufficient'); + const needsChanges = getCount(counts, 'Needs Changes'); + const didNotReview = getCount(counts, 'Did Not Review'); + + return { + reviewer: item.reviewer, + exceptional, + sufficient, + needsChanges, + didNotReview, + total: exceptional + sufficient + needsChanges + didNotReview, + }; +}; + +const CustomTooltip = ({ active, payload, label, darkMode }) => { + if (!active || !payload?.length) return null; + + const textColor = darkMode ? '#f8fafc' : '#111827'; + + return ( +
+ {label} + {payload.map(entry => ( +
+ + + {entry.value} +
+ ))} +
+ ); +}; + +const ReviewersRequirementChart = ({ duration, darkMode = false }) => { + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const yAxisWidth = useYAxisWidth(); + + useEffect(() => { + const fetchAPIData = async () => { + setIsLoading(true); + setErrorMessage(''); + + try { + const token = window.localStorage.getItem('token'); + const response = await axios.get(ENDPOINTS.GITHUB_REVIEW_SUMMARY(duration), { + headers: { + Authorization: token, + 'Content-Type': 'application/json', + }, + }); + + setData(Array.isArray(response.data) ? response.data : []); + } catch (err) { + setData([]); + setErrorMessage('Unable to load GitHub review data for the selected duration.'); + } finally { + setIsLoading(false); + } + }; + + fetchAPIData(); + }, [duration]); + + const sortedData = data + .map(normalizeReviewCounts) + .filter(item => item.reviewer) + .sort((a, b) => b.total - a.total); + + if (isLoading) { + return

Loading review data...

; + } + + if (errorMessage) { + return

{errorMessage}

; + } + + if (!sortedData.length) { + return

No review data found for this duration.

; + } + + const axisColor = darkMode ? '#e2e8f0' : '#374151'; + const gridColor = darkMode ? '#4a5568' : '#e5e7eb'; + const labelColor = darkMode ? '#f8fafc' : '#111827'; + const rowHeight = 34; + const chartHeight = Math.max(sortedData.length * rowHeight + 80, 360); + + return ( +
+ + + + + (value?.length > 22 ? `${value.slice(0, 20)}…` : value)} + /> + } + /> + + + + + + + + + + +
+ ); +}; + +export default ReviewersRequirementChart; diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index ba7a177971..33c285c4f0 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -516,7 +516,7 @@ export function Header(props) { )} - + {TIMELOG} @@ -839,6 +839,14 @@ export function Header(props) { > PR Analytics + + Reviewers by Requirement + + + + + {/* /* for support team*/} diff --git a/src/utils/URL.js b/src/utils/URL.js index 73c277c374..bf771ffb02 100644 --- a/src/utils/URL.js +++ b/src/utils/URL.js @@ -688,6 +688,16 @@ export const ENDPOINTS = { //pull requests analysis PR_REVIEWS_INSIGHTS: `${APIEndpoint}/analytics/pr-review-insights`, + GITHUB_REVIEW_SUMMARY: (duration, sort = 'desc', team) => { + const params = new URLSearchParams({ + duration, + sort, + }); + if (team) { + params.set('team', team); + } + return `${APIEndpoint}/analytics/review-summary?${params.toString()}`; + }, PR_GRADING_CONFIG: `${APIEndpoint}/pr-grading-config`, WEEKLY_GRADING: `${APIEndpoint}/weekly-grading`, WEEKLY_GRADING_SAVE: `${APIEndpoint}/weekly-grading/save`,