From d56ad294c6bc5cf10a6994a8a0a943bf14ac4db5 Mon Sep 17 00:00:00 2001 From: Rahul Vyas Date: Fri, 3 Jul 2026 00:51:02 +0530 Subject: [PATCH] Added new column of stale issue ratio in Governance page and also added card for it. --- src/context/AppContext.jsx | 40 +++++++- src/pages/GovernancePage.jsx | 183 ++++++++++++++++++++++++++++------- 2 files changed, 184 insertions(+), 39 deletions(-) diff --git a/src/context/AppContext.jsx b/src/context/AppContext.jsx index 61027be..ca37de5 100644 --- a/src/context/AppContext.jsx +++ b/src/context/AppContext.jsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useState, useCallback, useEffect } from 'react' +import { createContext, useContext, useState, useCallback, useEffect, useMemo } from 'react' import { fetchOrg, fetchRepos, fetchContributors, fetchIssues, } from '../services/github' import { buildAnalyticalModel, getTopRepositories } from '../services/analytics' @@ -129,7 +129,7 @@ export function AppProvider({ children }) { if (!model || govLoading) return setGovLoading(true) const map = {} - const repos = pat? model.allRepos : model.allRepos.slice(0, 15) + const repos = pat? model.totalRepos : model.totalRepos.slice(0, 15) // Batches of 5 using Promise.allSettled for (let i = 0; i < repos.length; i += 5) { @@ -142,11 +142,45 @@ export function AppProvider({ children }) { setGovLoading(false) }, [model, pat, govLoading]) + const STALE_DAYS = 90 + + const staleRepoStats = useMemo(() => { + const now = Date.now() + + return Object.entries(issuesData || {}).map(([key, issues]) => { + const [org, repo] = key.split('/') + + const normalIssues = issues.filter(i => !i.pull_request) + + const openIssues = normalIssues.filter(i => i.state === 'open') + + const staleIssues = openIssues.filter(i => { + const updated = new Date(i.updated_at).getTime() + const diffDays = (now - updated) / (1000 * 60 * 60 * 24) + return diffDays >= STALE_DAYS + }) + + const ratio = + openIssues.length === 0 + ? 0 + : Math.round((staleIssues.length / openIssues.length) * 100) + + return { + id: key, + org, + repo, + ratio, + staleCount: staleIssues.length, + openCount: openIssues.length + } + }).sort((a, b) => b.ratio - a.ratio) + }, [issuesData]) + return ( {children} diff --git a/src/pages/GovernancePage.jsx b/src/pages/GovernancePage.jsx index daceb66..76a290c 100644 --- a/src/pages/GovernancePage.jsx +++ b/src/pages/GovernancePage.jsx @@ -6,14 +6,52 @@ import { C, PageTitle, EmptyOk } from '../components/UI' const TABS = [ { key: 'dead', label: 'Dead Issues' }, { key: 'zombie', label: 'Zombie PRs' }, - { key: 'risky', label: 'Risky Repos' }, + { key: 'stale', label: 'Stale Issues Ratio' }, { key: 'license', label: 'No License' }, ] +const getStatus = ratio => { + if (ratio <= 10) + return { + label: 'Excellent', + color: 'var(--green)', + bg: 'rgba(34,197,94,.12)' + } + + if (ratio <= 25) + return { + label: 'Healthy', + color: 'var(--amber)', + bg: 'rgba(250,204,21,.12)' + } + + if (ratio <= 40) + return { + label: 'Warning', + color: 'var(--orange)', + bg: 'rgba(251,146,60,.12)' + } + + return { + label: 'Critical', + color: 'var(--red)', + bg: 'rgba(239,68,68,.12)' + } +} + export default function GovernancePage() { - const { model, issuesData, runAudit, govLoading } = useApp() + const { model, issuesData, runAudit, govLoading, staleRepoStats } = useApp() const [tab, setTab] = useState('dead') + const ITEMS_PER_PAGE = 10 + const [stalePage, setStalePage] = useState(1) + const totalPages = Math.ceil(staleRepoStats.length / ITEMS_PER_PAGE) + + const paginatedStaleRepos = useMemo(() => { + const start = (stalePage - 1) * ITEMS_PER_PAGE + return staleRepoStats.slice(start, start + ITEMS_PER_PAGE) + }, [staleRepoStats, stalePage]) + console.log(paginatedStaleRepos); // Flatten all issues and tag with repo/org const allIssues = useMemo(() => { const arr = [] @@ -34,28 +72,21 @@ export default function GovernancePage() { .filter(i => !i.pull_request && daysSince(i.created_at) >= 90) .sort((a, b) => daysSince(b.created_at) - daysSince(a.created_at)) - // Health check 2 — Zombie PRs (>90 days open) + // Health check 2 — Percentage of dead issues relative to all issues + const staleIssuesRatio = allIssues.length ? (deadIssues.length / allIssues.length) * 100 : 0; + + // Health check 3 — Zombie PRs (>90 days open) const zombiePRs = allIssues .filter(i => i.pull_request && daysSince(i.created_at) >= 90) .sort((a, b) => daysSince(b.created_at) - daysSince(a.created_at)) - // Health check 3 — Risky repos (top-2 contributor concentration >80%) - const riskyRepos = model.allRepos.filter(r => { - const c = r.contributors || [] - if (!c.length) return false - const total = c.reduce((s, x) => s + x.contributions, 0) - if (!total) return false - const topTwo = (c[0]?.contributions || 0) + (c[1]?.contributions || 0) - return topTwo / total > 0.8 - }) - // Health check 4 — No license const noLicense = model.allRepos.filter(r => !r.license && !r.archived && !r.fork) // Issue resolution rate per repo const topRepos = model.allRepos.slice(0, 8) - const counts = { dead: deadIssues.length, zombie: zombiePRs.length, risky: riskyRepos.length, license: noLicense.length } + const counts = { dead: deadIssues.length, zombie: zombiePRs.length, license: noLicense.length, stale: staleIssuesRatio.toFixed(2) } // Stat card const StatBox = ({ label, value, sub, color }) => ( @@ -124,8 +155,8 @@ export default function GovernancePage() { {/* Summary stat cards */}
+ -
@@ -182,7 +213,7 @@ export default function GovernancePage() { }} > {t.label}{' '} - 0 ? 'var(--red)' : 'var(--green)', marginLeft: 4 }}> + 40 ? 'var(--red)' : 'var(--green)', marginLeft: 4 }}> {counts[t.key]} @@ -213,28 +244,108 @@ export default function GovernancePage() { ) : )} - {/* Risky Repos */} - {tab === 'risky' && ( - riskyRepos.length ? ( -
- {riskyRepos.map(r => { - const c = r.contributors || [] - const total = c.reduce((s, x) => s + x.contributions, 0) || 1 - const pct = Math.round(((c[0]?.contributions || 0) + (c[1]?.contributions || 0)) / total * 100) - return ( -
-
-
{r.name}
-
- Top 2 contributors own {pct}% of all commits — concentration risk + {/* Stale Issues */} + {tab === 'stale' && ( + staleRepoStats.length ? ( + <> +
+ {paginatedStaleRepos.map(repo => { + const status = getStatus(repo.ratio) + + return ( +
+
+
+ {repo.repo} +
+ +
+ {repo.staleCount} stale issues out of{' '} + {repo.openCount} open issues ({repo.ratio}%) +
+
+ +
+ + {status.label} + + + GitHub +
- HIGH RISK -
- ) - })} -
- ) : + ) + })} +
+ + {totalPages > 1 && ( +
+ + + + Page {stalePage} of {totalPages} + + + +
+ )} + + ) : ( + + ) )} {/* No License */}