From 23f849d6fc25af2fc538add622e1ceffc1681b89 Mon Sep 17 00:00:00 2001 From: aaniya22 Date: Thu, 13 Aug 2026 14:27:54 +0530 Subject: [PATCH] feat: add colored Lines Changed stat tile to PR Analytics widget (#2450) Signed-off-by: aaniya22 --- src/components/PRMetrics.tsx | 256 +++++++++++++++++++++++++---------- 1 file changed, 188 insertions(+), 68 deletions(-) diff --git a/src/components/PRMetrics.tsx b/src/components/PRMetrics.tsx index 523e6c00a..f651d7b85 100644 --- a/src/components/PRMetrics.tsx +++ b/src/components/PRMetrics.tsx @@ -1,11 +1,18 @@ "use client"; import SectionHeader from "./SectionHeader"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState, type ReactNode } from "react"; import { usePersistentState } from "@/hooks/usePersistentState"; import { useAccount } from "@/components/AccountContext"; import { useDashboardWidgetA11y } from "@/components/dashboard/DashboardWidgetA11yContext"; -import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts"; +import { + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, +} from "recharts"; import PRStatusDonutChart from "./PRStatusDonutChart"; import MiniPRTrendChart from "./MiniPRTrendChart"; import { SkeletonBlock } from "./WidgetSkeleton"; @@ -36,7 +43,7 @@ interface PRData extends PRMetricsSummary { interface PRStat { label: string; - value: string | number; + value: string | number | ReactNode; title?: string; warning?: boolean; href?: string; @@ -55,10 +62,22 @@ export default function PRMetrics() { const [lastUpdated, setLastUpdated] = useState(null); const [minutesAgo, setMinutesAgo] = useState(0); const [error, setError] = useState(null); - const [activeTab, setActiveTab] = usePersistentState<"authored" | "reviews">("devtrack:pr-metrics:activeTab", "authored"); - const [prFilter, setPrFilter] = usePersistentState<"all" | "merged" | "open">("devtrack:pr-metrics:prFilter", "all"); - const [range, setRange] = usePersistentState<"7d" | "30d" | "90d">("devtrack:pr-metrics:range", "30d"); - const [staleThresholdDays, setStaleThresholdDays] = usePersistentState("devtrack:pr-metrics:staleThreshold", 14); + const [activeTab, setActiveTab] = usePersistentState<"authored" | "reviews">( + "devtrack:pr-metrics:activeTab", + "authored" + ); + const [prFilter, setPrFilter] = usePersistentState<"all" | "merged" | "open">( + "devtrack:pr-metrics:prFilter", + "all" + ); + const [range, setRange] = usePersistentState<"7d" | "30d" | "90d">( + "devtrack:pr-metrics:range", + "30d" + ); + const [staleThresholdDays, setStaleThresholdDays] = usePersistentState( + "devtrack:pr-metrics:staleThreshold", + 14 + ); const fetchMetrics = useCallback(() => { setLoading(true); @@ -79,7 +98,11 @@ export default function PRMetrics() { setLastUpdated(new Date()); setMinutesAgo(0); }) - .catch(() => setError("We couldn't load your PR analytics right now. Please try again in a moment.")) + .catch(() => + setError( + "We couldn't load your PR analytics right now. Please try again in a moment." + ) + ) .finally(() => setLoading(false)); }, [selectedAccount, range]); @@ -112,12 +135,23 @@ export default function PRMetrics() { { label: labels.merged, value: source.merged }, { label: "Lines Changed", - value: `+${(source.totalAdditions ?? 0).toLocaleString()} / -${(source.totalDeletions ?? 0).toLocaleString()}` + value: ( + + + +{(source.totalAdditions ?? 0).toLocaleString()} + + {" / "} + + -{(source.totalDeletions ?? 0).toLocaleString()} + + + ), }, { label: `${labels.avgReview} ⓘ`, value: `${source.avgReviewHours}h`, - title: "Average time from PR creation to close, based on your last 30 closed PRs", + title: + "Average time from PR creation to close, based on your last 30 closed PRs", }, { label: labels.avgFirstReview, @@ -128,49 +162,64 @@ export default function PRMetrics() { ]; if (labels.avgCycleTime && source.avgCycleTime !== undefined) { - baseStats.push({ label: labels.avgCycleTime, value: `${source.avgCycleTime}h` }); + baseStats.push({ + label: labels.avgCycleTime, + value: `${source.avgCycleTime}h`, + }); } return baseStats; }; const githubStats = metrics ? buildStats(metrics, { - open: "Open PRs", - merged: `Merged (${range}d)`, - avgReview: "Avg Review Time", - avgFirstReview: "Avg First Review", - mergeRate: "Merge Rate", - avgCycleTime: "Avg Cycle Time", - }) + open: "Open PRs", + merged: `Merged (${range}d)`, + avgReview: "Avg Review Time", + avgFirstReview: "Avg First Review", + mergeRate: "Merge Rate", + avgCycleTime: "Avg Cycle Time", + }) : []; const gitlabStats = metrics?.gitlab ? buildStats(metrics.gitlab, { - open: "Open MRs", - merged: `Merged (${range}d)`, - avgReview: "Avg Review Time", - avgFirstReview: "Avg First Review", - mergeRate: "Merge Rate", - }) + open: "Open MRs", + merged: `Merged (${range}d)`, + avgReview: "Avg Review Time", + avgFirstReview: "Avg First Review", + mergeRate: "Merge Rate", + }) : []; const renderStat = (stat: PRStat) => { const content = ( <> -
+
{stat.value}
-
{stat.label}
+
+ {stat.label} +
); - const className = `rounded-lg p-4 text-center min-w-0 border border-transparent transition-all duration-300 ease-out hover:-translate-y-1 hover:shadow-md ${stat.warning - ? "border-orange-400/30 bg-orange-500/10 hover:bg-orange-500/15 hover:border-orange-400/50" - : "bg-[var(--control)] hover:bg-[var(--control-hover)] hover:border-[var(--border)]" - }`; + const className = `rounded-lg p-4 text-center min-w-0 border border-transparent transition-all duration-300 ease-out hover:-translate-y-1 hover:shadow-md ${ + stat.warning + ? "border-orange-400/30 bg-orange-500/10 hover:bg-orange-500/15 hover:border-orange-400/50" + : "bg-[var(--control)] hover:bg-[var(--control-hover)] hover:border-[var(--border)]" + }`; return stat.href ? ( - + {content} ) : ( @@ -190,10 +239,11 @@ export default function PRMetrics() { @@ -202,15 +252,21 @@ export default function PRMetrics() {
@@ -219,7 +275,9 @@ export default function PRMetrics() { Range setStaleThresholdDays(Number(event.target.value))} + onChange={(event) => + setStaleThresholdDays(Number(event.target.value)) + } className="rounded-md border border-[var(--border)] bg-[var(--control)] px-2 py-1 text-sm text-[var(--foreground)] transition-colors" > {[7, 14, 30].map((days) => ( - + ))} @@ -262,7 +324,10 @@ export default function PRMetrics() { ) : error ? (

{error}

-
@@ -271,14 +336,19 @@ export default function PRMetrics() { {/* GitHub Stats */}
-

GitHub PRs

+

+ GitHub PRs +

{(["all", "merged", "open"] as const).map((filter) => ( @@ -290,8 +360,14 @@ export default function PRMetrics() { .filter((stat) => { if (prFilter === "all") return true; const lbl = stat.label.toLowerCase(); - if (prFilter === "open") return lbl.includes("open") || lbl.includes("stale"); - if (prFilter === "merged") return lbl.includes("merged") || lbl.includes("review") || lbl.includes("merge rate"); + if (prFilter === "open") + return lbl.includes("open") || lbl.includes("stale"); + if (prFilter === "merged") + return ( + lbl.includes("merged") || + lbl.includes("review") || + lbl.includes("merge rate") + ); return true; }) .map(renderStat)} @@ -302,11 +378,13 @@ export default function PRMetrics() { {/* PR Status Donut Chart */} {metrics && (
-

PR Status Distribution

+

+ PR Status Distribution +

)} @@ -315,13 +393,23 @@ export default function PRMetrics() {
{metrics?.weeklyTrend && metrics.weeklyTrend.length > 0 && (
-

Review Cycle Trend

+

+ Review Cycle Trend +

- [`${val}h`, "Avg Cycle Time"]} /> - + [`${val}h`, "Avg Cycle Time"]} + /> +
@@ -329,12 +417,21 @@ export default function PRMetrics() { {metrics?.slowestRepos && metrics.slowestRepos.length > 0 && (
-

Slowest Review Repos

+

+ Slowest Review Repos +

{metrics.slowestRepos.map((r) => ( -
- {r.repo} - {r.avgHours}h +
+ + {r.repo} + + + {r.avgHours}h +
))}
@@ -345,14 +442,22 @@ export default function PRMetrics() { {/* GitLab MRs Section */} {metrics?.gitlab && (
-

GitLab MRs

+

+ GitLab MRs +

{gitlabStats .filter((stat) => { if (prFilter === "all") return true; const lbl = stat.label.toLowerCase(); - if (prFilter === "open") return lbl.includes("open") || lbl.includes("stale"); - if (prFilter === "merged") return lbl.includes("merged") || lbl.includes("review") || lbl.includes("merge rate"); + if (prFilter === "open") + return lbl.includes("open") || lbl.includes("stale"); + if (prFilter === "merged") + return ( + lbl.includes("merged") || + lbl.includes("review") || + lbl.includes("merge rate") + ); return true; }) .map(renderStat)} @@ -364,12 +469,25 @@ export default function PRMetrics() {
{[ - { label: "Total Reviews Given", value: metrics?.reviews?.totalReviews ?? 0 }, - { label: "Approval Rate", value: metrics?.reviews?.approvalRate ?? "0%" }, + { + label: "Total Reviews Given", + value: metrics?.reviews?.totalReviews ?? 0, + }, + { + label: "Approval Rate", + value: metrics?.reviews?.approvalRate ?? "0%", + }, ].map((stat) => ( -
-
{stat.value}
-
{stat.label}
+
+
+ {stat.value} +
+
+ {stat.label} +
))}
@@ -378,7 +496,9 @@ export default function PRMetrics() { {lastUpdated && (

- {minutesAgo === 0 ? "Updated just now" : `Updated ${minutesAgo} min ago`} + {minutesAgo === 0 + ? "Updated just now" + : `Updated ${minutesAgo} min ago`}

)}