-
-
Notifications
You must be signed in to change notification settings - Fork 90
feat(governance): implement Repository Health & Risk Scorecard (Gover… #224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import React from 'react' | ||
|
|
||
| export default function RadialGauge({ score, size = 80, strokeWidth = 7 }) { | ||
| const center = size / 2 | ||
| const radius = center - strokeWidth | ||
| const circumference = 2 * Math.PI * radius | ||
|
|
||
| const validScore = typeof score === 'number' && !isNaN(score) ? Math.min(100, Math.max(0, score)) : null | ||
| const offset = validScore !== null ? circumference - (validScore / 100) * circumference : circumference | ||
|
|
||
| let color = 'var(--text3)' | ||
| let bgStroke = 'var(--border)' | ||
|
|
||
| if (validScore !== null) { | ||
| if (validScore >= 70) color = 'var(--green, #22c55e)' | ||
| else if (validScore >= 40) color = 'var(--amber, #f59e0b)' | ||
| else color = 'var(--red, #ef4444)' | ||
| } | ||
|
|
||
| return ( | ||
| <div style={{ position: 'relative', width: size, height: size, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}> | ||
| <svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}> | ||
| {/* Background track */} | ||
| <circle | ||
| cx={center} | ||
| cy={center} | ||
| r={radius} | ||
| fill="none" | ||
| stroke={bgStroke} | ||
| strokeWidth={strokeWidth} | ||
| /> | ||
| {/* Foreground progress */} | ||
| {validScore !== null && ( | ||
| <circle | ||
| cx={center} | ||
| cy={center} | ||
| r={radius} | ||
| fill="none" | ||
| stroke={color} | ||
| strokeWidth={strokeWidth} | ||
| strokeDasharray={circumference} | ||
| strokeDashoffset={offset} | ||
| strokeLinecap="round" | ||
| style={{ transition: 'stroke-dashoffset 0.6s ease-out' }} | ||
| /> | ||
| )} | ||
| </svg> | ||
| <div style={{ position: 'absolute', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}> | ||
| <span style={{ fontSize: size * 0.28, fontWeight: 700, color: validScore !== null ? color : 'var(--text2)', lineHeight: 1 }}> | ||
| {validScore !== null ? validScore : 'N/A'} | ||
| </span> | ||
| {validScore !== null && ( | ||
| <span style={{ fontSize: size * 0.13, color: 'var(--text3)', marginTop: 2, fontWeight: 500 }}> | ||
| / 100 | ||
| </span> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import { createContext, useContext, useState, useCallback, useEffect, useMemo, useRef } from 'react' | ||
| import { fetchOrg, fetchRepos, fetchContributors, fetchIssues, fetchRateLimit, fetchPulls } from '../services/github' | ||
| import { buildAnalyticalModel, getTopRepositories } from '../services/analytics' | ||
| import { buildAnalyticalModel, getTopRepositories, computeRepoHealthScore } from '../services/analytics' | ||
| import { saveAnalysis, loadAnalysis } from '../services/cache' | ||
|
|
||
| const Ctx = createContext(null) | ||
|
|
@@ -377,14 +377,31 @@ export function AppProvider({ children }) { | |
| }).sort((a, b) => b.ratio - a.ratio) | ||
| }, [issuesData]) | ||
|
|
||
| const repoScorecards = useMemo(() => { | ||
| if (!model || !model.totalRepos) return [] | ||
| return model.totalRepos | ||
| .map(repo => { | ||
| const key = `${repo.orgLogin}/${repo.name}` | ||
| const issues = issuesData[key] || [] | ||
| const pulls = pullsData[key] || [] | ||
| return computeRepoHealthScore(repo, issues, pulls) | ||
| }) | ||
| .sort((a, b) => { | ||
| if (a.overallScore === null && b.overallScore === null) return 0 | ||
| if (a.overallScore === null) return 1 | ||
| if (b.overallScore === null) return -1 | ||
| return a.overallScore - b.overallScore | ||
| }) | ||
| }, [model, issuesData, pullsData]) | ||
|
|
||
| return ( | ||
| <Ctx.Provider value={{ | ||
| pat, savePat, orgs, model, issuesData, pullsData, | ||
| rateLimit, loading, loadMsg, govLoading, error, totalRepo, | ||
| runAdvanceAnalytics, refreshRateLimit, advanceAnalyticsLoading, advanceAnalyticsComplete, | ||
| runFullAnalytics, | ||
| isComplete, auditComplete, lastOrgNames, hydrating, | ||
| explore, runFullExplore, runAudit, runGovernanceAnalysis, setError, staleRepoStats | ||
| explore, runFullExplore, runAudit, runGovernanceAnalysis, setError, staleRepoStats, repoScorecards | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win Memoize the context value so the new scorecards do not re-render every consumer. Line 398 builds the provider value as an inline object literal, so the value identity changes on every This change makes that cost higher. Wrap the provider value in ♻️ Proposed refactor+ const ctxValue = useMemo(() => ({
+ pat, savePat, orgs, model, issuesData, pullsData,
+ rateLimit, loading, loadMsg, govLoading, error, totalRepo,
+ runAdvanceAnalytics, refreshRateLimit, advanceAnalyticsLoading, advanceAnalyticsComplete,
+ runFullAnalytics,
+ isComplete, auditComplete, lastOrgNames, hydrating,
+ explore, runFullExplore, runAudit, runGovernanceAnalysis, setError, staleRepoStats, repoScorecards
+ }), [
+ pat, savePat, orgs, model, issuesData, pullsData,
+ rateLimit, loading, loadMsg, govLoading, error, totalRepo,
+ runAdvanceAnalytics, refreshRateLimit, advanceAnalyticsLoading, advanceAnalyticsComplete,
+ runFullAnalytics, isComplete, auditComplete, lastOrgNames, hydrating,
+ explore, runFullExplore, runAudit, runGovernanceAnalysis, staleRepoStats, repoScorecards
+ ])
+
return (
- <Ctx.Provider value={{
- pat, savePat, orgs, model, issuesData, pullsData,
- rateLimit, loading, loadMsg, govLoading, error, totalRepo,
- runAdvanceAnalytics, refreshRateLimit, advanceAnalyticsLoading, advanceAnalyticsComplete,
- runFullAnalytics,
- isComplete, auditComplete, lastOrgNames, hydrating,
- explore, runFullExplore, runAudit, runGovernanceAnalysis, setError, staleRepoStats, repoScorecards
- }}>
+ <Ctx.Provider value={ctxValue}>
{children}
</Ctx.Provider>
)🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| }}> | ||
| {children} | ||
| </Ctx.Provider> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Hide the decorative SVG from assistive technology.
The score and the
/ 100suffix are already rendered as text on lines 49-56, so assistive technology reads the value. The<svg>on line 22 duplicates that information visually and has no accessible name. Addaria-hidden="true"andfocusable="false"so it is not announced as an unlabeled graphic.♿ Proposed refactor
📝 Committable suggestion
🤖 Prompt for AI Agents