Skip to content

Commit a91f8a4

Browse files
Merge pull request #95 from rahul-vyas-dev/feat/stale-issue-ratio
Added new column of stale issue ratio in Governance page and also add…
2 parents ff17681 + d56ad29 commit a91f8a4

2 files changed

Lines changed: 184 additions & 39 deletions

File tree

src/context/AppContext.jsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, useContext, useState, useCallback, useEffect } from 'react'
1+
import { createContext, useContext, useState, useCallback, useEffect, useMemo } from 'react'
22
import { fetchOrg, fetchRepos, fetchContributors, fetchIssues, } from '../services/github'
33
import { buildAnalyticalModel, getTopRepositories } from '../services/analytics'
44

@@ -129,7 +129,7 @@ export function AppProvider({ children }) {
129129
if (!model || govLoading) return
130130
setGovLoading(true)
131131
const map = {}
132-
const repos = pat? model.allRepos : model.allRepos.slice(0, 15)
132+
const repos = pat? model.totalRepos : model.totalRepos.slice(0, 15)
133133

134134
// Batches of 5 using Promise.allSettled
135135
for (let i = 0; i < repos.length; i += 5) {
@@ -142,11 +142,45 @@ export function AppProvider({ children }) {
142142
setGovLoading(false)
143143
}, [model, pat, govLoading])
144144

145+
const STALE_DAYS = 90
146+
147+
const staleRepoStats = useMemo(() => {
148+
const now = Date.now()
149+
150+
return Object.entries(issuesData || {}).map(([key, issues]) => {
151+
const [org, repo] = key.split('/')
152+
153+
const normalIssues = issues.filter(i => !i.pull_request)
154+
155+
const openIssues = normalIssues.filter(i => i.state === 'open')
156+
157+
const staleIssues = openIssues.filter(i => {
158+
const updated = new Date(i.updated_at).getTime()
159+
const diffDays = (now - updated) / (1000 * 60 * 60 * 24)
160+
return diffDays >= STALE_DAYS
161+
})
162+
163+
const ratio =
164+
openIssues.length === 0
165+
? 0
166+
: Math.round((staleIssues.length / openIssues.length) * 100)
167+
168+
return {
169+
id: key,
170+
org,
171+
repo,
172+
ratio,
173+
staleCount: staleIssues.length,
174+
openCount: openIssues.length
175+
}
176+
}).sort((a, b) => b.ratio - a.ratio)
177+
}, [issuesData])
178+
145179
return (
146180
<Ctx.Provider value={{
147181
pat, savePat, orgs, model, issuesData,
148182
rateLimit, loading, loadMsg, govLoading, error, totalRepo,
149-
explore, runAudit, setError,
183+
explore, runAudit, setError, staleRepoStats,
150184
}}>
151185
{children}
152186
</Ctx.Provider>

src/pages/GovernancePage.jsx

Lines changed: 147 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,52 @@ import { C, PageTitle, EmptyOk } from '../components/UI'
66
const TABS = [
77
{ key: 'dead', label: 'Dead Issues' },
88
{ key: 'zombie', label: 'Zombie PRs' },
9-
{ key: 'risky', label: 'Risky Repos' },
9+
{ key: 'stale', label: 'Stale Issues Ratio' },
1010
{ key: 'license', label: 'No License' },
1111
]
1212

13+
const getStatus = ratio => {
14+
if (ratio <= 10)
15+
return {
16+
label: 'Excellent',
17+
color: 'var(--green)',
18+
bg: 'rgba(34,197,94,.12)'
19+
}
20+
21+
if (ratio <= 25)
22+
return {
23+
label: 'Healthy',
24+
color: 'var(--amber)',
25+
bg: 'rgba(250,204,21,.12)'
26+
}
27+
28+
if (ratio <= 40)
29+
return {
30+
label: 'Warning',
31+
color: 'var(--orange)',
32+
bg: 'rgba(251,146,60,.12)'
33+
}
34+
35+
return {
36+
label: 'Critical',
37+
color: 'var(--red)',
38+
bg: 'rgba(239,68,68,.12)'
39+
}
40+
}
41+
1342
export default function GovernancePage() {
14-
const { model, issuesData, runAudit, govLoading } = useApp()
43+
const { model, issuesData, runAudit, govLoading, staleRepoStats } = useApp()
1544
const [tab, setTab] = useState('dead')
1645

46+
const ITEMS_PER_PAGE = 10
47+
const [stalePage, setStalePage] = useState(1)
48+
const totalPages = Math.ceil(staleRepoStats.length / ITEMS_PER_PAGE)
49+
50+
const paginatedStaleRepos = useMemo(() => {
51+
const start = (stalePage - 1) * ITEMS_PER_PAGE
52+
return staleRepoStats.slice(start, start + ITEMS_PER_PAGE)
53+
}, [staleRepoStats, stalePage])
54+
console.log(paginatedStaleRepos);
1755
// Flatten all issues and tag with repo/org
1856
const allIssues = useMemo(() => {
1957
const arr = []
@@ -34,28 +72,21 @@ export default function GovernancePage() {
3472
.filter(i => !i.pull_request && daysSince(i.created_at) >= 90)
3573
.sort((a, b) => daysSince(b.created_at) - daysSince(a.created_at))
3674

37-
// Health check 2 — Zombie PRs (>90 days open)
75+
// Health check 2 — Percentage of dead issues relative to all issues
76+
const staleIssuesRatio = allIssues.length ? (deadIssues.length / allIssues.length) * 100 : 0;
77+
78+
// Health check 3 — Zombie PRs (>90 days open)
3879
const zombiePRs = allIssues
3980
.filter(i => i.pull_request && daysSince(i.created_at) >= 90)
4081
.sort((a, b) => daysSince(b.created_at) - daysSince(a.created_at))
4182

42-
// Health check 3 — Risky repos (top-2 contributor concentration >80%)
43-
const riskyRepos = model.allRepos.filter(r => {
44-
const c = r.contributors || []
45-
if (!c.length) return false
46-
const total = c.reduce((s, x) => s + x.contributions, 0)
47-
if (!total) return false
48-
const topTwo = (c[0]?.contributions || 0) + (c[1]?.contributions || 0)
49-
return topTwo / total > 0.8
50-
})
51-
5283
// Health check 4 — No license
5384
const noLicense = model.allRepos.filter(r => !r.license && !r.archived && !r.fork)
5485

5586
// Issue resolution rate per repo
5687
const topRepos = model.allRepos.slice(0, 8)
5788

58-
const counts = { dead: deadIssues.length, zombie: zombiePRs.length, risky: riskyRepos.length, license: noLicense.length }
89+
const counts = { dead: deadIssues.length, zombie: zombiePRs.length, license: noLicense.length, stale: staleIssuesRatio.toFixed(2) }
5990

6091
// Stat card
6192
const StatBox = ({ label, value, sub, color }) => (
@@ -124,8 +155,8 @@ export default function GovernancePage() {
124155
{/* Summary stat cards */}
125156
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 12, marginBottom: 24 }}>
126157
<StatBox label="Dead Issues" value={counts.dead} sub="OPEN 90+ DAYS" color="var(--red)" />
158+
<StatBox label="Stale Issues Ratio" value={`${staleIssuesRatio.toFixed(2)}%`} sub={`of ${allIssues.length} total issues`} color={` ${getStatus(staleIssuesRatio).color}`} />
127159
<StatBox label="Zombie PRs" value={counts.zombie} sub="PENDING 90+ DAYS" color="var(--amber)" />
128-
<StatBox label="Risky Repos" value={counts.risky} sub="TOP-2 CONCENTRATION" color="var(--amber)" />
129160
<StatBox label="No License" value={counts.license} sub="COMPLIANCE MISSING" color="var(--text2)" />
130161
</div>
131162

@@ -182,7 +213,7 @@ export default function GovernancePage() {
182213
}}
183214
>
184215
{t.label}{' '}
185-
<span style={{ color: counts[t.key] > 0 ? 'var(--red)' : 'var(--green)', marginLeft: 4 }}>
216+
<span style={{ color: counts[t.key] > 40 ? 'var(--red)' : 'var(--green)', marginLeft: 4 }}>
186217
{counts[t.key]}
187218
</span>
188219
</button>
@@ -213,28 +244,108 @@ export default function GovernancePage() {
213244
) : <EmptyOk msg="No zombie PRs found" sub="This org reviews and closes contributions actively." />
214245
)}
215246

216-
{/* Risky Repos */}
217-
{tab === 'risky' && (
218-
riskyRepos.length ? (
219-
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
220-
{riskyRepos.map(r => {
221-
const c = r.contributors || []
222-
const total = c.reduce((s, x) => s + x.contributions, 0) || 1
223-
const pct = Math.round(((c[0]?.contributions || 0) + (c[1]?.contributions || 0)) / total * 100)
224-
return (
225-
<div key={r.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 14px', background: 'var(--surface2)', borderRadius: 6 }}>
226-
<div>
227-
<div style={{ fontWeight: 500, fontSize: 13 }}>{r.name}</div>
228-
<div style={{ fontSize: 12, color: 'var(--text2)', marginTop: 2 }}>
229-
Top 2 contributors own {pct}% of all commits — concentration risk
247+
{/* Stale Issues */}
248+
{tab === 'stale' && (
249+
staleRepoStats.length ? (
250+
<>
251+
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
252+
{paginatedStaleRepos.map(repo => {
253+
const status = getStatus(repo.ratio)
254+
255+
return (
256+
<div
257+
key={repo.id}
258+
style={{
259+
display: 'flex',
260+
justifyContent: 'space-between',
261+
alignItems: 'center',
262+
padding: '12px 14px',
263+
background: 'var(--surface2)',
264+
borderRadius: 6
265+
}}
266+
>
267+
<div>
268+
<div style={{ fontWeight: 600 }}>
269+
{repo.repo}
270+
</div>
271+
272+
<div
273+
style={{
274+
marginTop: 4,
275+
fontSize: 12,
276+
color: 'var(--text2)'
277+
}}
278+
>
279+
{repo.staleCount} stale issues out of{' '}
280+
{repo.openCount} open issues ({repo.ratio}%)
281+
</div>
282+
</div>
283+
284+
<div
285+
style={{
286+
display: 'flex',
287+
alignItems: 'center',
288+
gap: 30
289+
}}
290+
>
291+
<span style={C.pill(status.color, status.bg)}>
292+
{status.label}
293+
</span>
294+
<a href={`https://github.com/${repo.org}/${repo.repo}/issues`} target="_blank" rel="noreferrer"
295+
style={{ fontSize: 12, color: 'var(--text2)', display: 'flex', alignItems: 'center', gap: 4 }}>
296+
<FiExternalLink size={12} /> GitHub
297+
</a>
230298
</div>
231299
</div>
232-
<span style={C.pill('var(--red)', 'rgba(239,68,68,.12)')}>HIGH RISK</span>
233-
</div>
234-
)
235-
})}
236-
</div>
237-
) : <EmptyOk msg="No high-concentration repos found" sub="Healthy contributor distribution across the portfolio." />
300+
)
301+
})}
302+
</div>
303+
304+
{totalPages > 1 && (
305+
<div
306+
style={{
307+
display: 'flex',
308+
justifyContent: 'space-between',
309+
alignItems: 'center',
310+
marginTop: 16
311+
}}
312+
>
313+
<button
314+
onClick={() => setStalePage(p => p - 1)}
315+
disabled={stalePage === 1}
316+
style={{
317+
padding: '8px 14px',
318+
cursor: stalePage === 1 ? 'not-allowed' : 'pointer',
319+
opacity: stalePage === 1 ? 0.5 : 1
320+
}}
321+
>
322+
← Previous
323+
</button>
324+
325+
<span style={{ fontSize: 13, color: 'var(--text2)' }}>
326+
Page {stalePage} of {totalPages}
327+
</span>
328+
329+
<button
330+
onClick={() => setStalePage(p => p + 1)}
331+
disabled={stalePage === totalPages}
332+
style={{
333+
padding: '8px 14px',
334+
cursor: stalePage === totalPages ? 'not-allowed' : 'pointer',
335+
opacity: stalePage === totalPages ? 0.5 : 1
336+
}}
337+
>
338+
Next →
339+
</button>
340+
</div>
341+
)}
342+
</>
343+
) : (
344+
<EmptyOk
345+
msg="No stale issues found"
346+
sub="All repositories have active open issues."
347+
/>
348+
)
238349
)}
239350

240351
{/* No License */}

0 commit comments

Comments
 (0)