forked from AOSSIE-Org/OrgExplorer
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAppContext.jsx
More file actions
155 lines (127 loc) · 5.04 KB
/
Copy pathAppContext.jsx
File metadata and controls
155 lines (127 loc) · 5.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import { createContext, useContext, useState, useCallback, useEffect } from 'react'
import { fetchOrg, fetchRepos, fetchContributors, fetchIssues, fetchRateLimit } from '../services/github'
import { buildAnalyticalModel, getTopRepositories } from '../services/analytics'
const Ctx = createContext(null)
function getStoredRateLimit() {
const stored = localStorage.getItem('oe_rate_limit')
if (!stored) return null
try {
const data = JSON.parse(stored)
if (Date.now() > data.reset * 1000) {
localStorage.removeItem('oe_rate_limit')
return null
}
return data
} catch {
localStorage.removeItem('oe_rate_limit')
return null
}
}
export function AppProvider({ children }) {
const [pat, setPat] = useState(() => localStorage.getItem('oe_pat') || '')
const [orgs, setOrgs] = useState([])
const [model, setModel] = useState(null)
const [issuesData, setIssuesData] = useState({})
const [rateLimit, setRateLimit] = useState(getStoredRateLimit)
const [loading, setLoading] = useState(false)
const [loadMsg, setLoadMsg] = useState('')
const [govLoading, setGovLoading] = useState(false)
const [error, setError] = useState('')
const [totalRepo, setTotalRepo] = useState(0)
useEffect(() => {
const handler = e => {
setRateLimit(e.detail)
localStorage.setItem('oe_rate_limit', JSON.stringify(e.detail))
}
window.addEventListener('rate-limit-update', handler)
return () => {
window.removeEventListener('rate-limit-update', handler)
}
}, [])
useEffect(() => {
if (!rateLimit?.reset) return
const timeout = setTimeout(() => {
localStorage.removeItem('oe_rate_limit')
setRateLimit(null)
}, Math.max(0, rateLimit.reset * 1000 - Date.now()))
return () => clearTimeout(timeout)
}, [rateLimit])
const refreshRateLimit = useCallback(async () => {
const rl = await fetchRateLimit(pat)
if (rl) setRateLimit(rl)
}, [pat])
const savePat = useCallback(token => {
setPat(token)
token ? localStorage.setItem('oe_pat', token) : localStorage.removeItem('oe_pat')
}, [])
// Multi-org explore — core of Section 3.2.0
const explore = useCallback(async orgNames => {
setLoading(true); setError(''); setModel(null); setOrgs([]); setIssuesData({})
try {
setLoadMsg('Fetching organization metadata...')
const orgRes = await Promise.allSettled(orgNames.map(n => fetchOrg(n, pat)))
const validOrgs = orgRes.filter(r => r.status === 'fulfilled').map(r => r.value)
if (!validOrgs.length) throw new Error('No valid organizations found. Check the names and try again.')
setOrgs(validOrgs)
setLoadMsg('Fetching repositories...')
const reposPerOrg = {}
await Promise.allSettled(validOrgs.map(async org => {
reposPerOrg[org.login] = await fetchRepos(org.login, org.public_repos, pat)
}))
const total = Object.values(reposPerOrg).reduce(
(sum, repos) => sum + repos.length,
0
);
setTotalRepo(total);
setLoadMsg('Fetching contributor data for top repositories...')
const contribsPerRepo = {}
for (const org of validOrgs) {
const top = pat ? (reposPerOrg[org.login] || []) : getTopRepositories(reposPerOrg[org.login] || [], 10);
reposPerOrg[org.login] = top; // Update to only include top repos
await Promise.allSettled(top.map(async repo => {
contribsPerRepo[`${org.login}/${repo.name}`] = await fetchContributors(org.login, repo.name, pat)
}))
}
setLoadMsg('Building analytical data model...')
setModel(buildAnalyticalModel(validOrgs, reposPerOrg, contribsPerRepo))
// Save to recent searches
const prev = JSON.parse(localStorage.getItem('oe_recent') || '[]')
const entry = orgNames.join(', ')
localStorage.setItem('oe_recent', JSON.stringify([...new Set([entry, ...prev])].slice(0, 6)))
return true
} catch (err) {
setError(err.message === 'RATE_LIMIT'
? 'GitHub API rate limit reached. Add a PAT in Settings for 5,000 req/hr.'
: err.message)
return false
} finally {
setLoading(false); setLoadMsg('')
}
}, [pat])
// Governance audit — parallel batches of 5 (Section 3.2.5)
const runAudit = useCallback(async () => {
if (!model || govLoading) return
setGovLoading(true)
const map = {}
const repos = pat? model.allRepos : model.allRepos.slice(0, 15)
// Batches of 5 using Promise.allSettled
for (let i = 0; i < repos.length; i += 5) {
const batch = repos.slice(i, i + 5)
await Promise.allSettled(batch.map(async repo => {
map[`${repo.orgLogin}/${repo.name}`] = await fetchIssues(repo.orgLogin, repo.name, pat)
}))
}
setIssuesData(map)
setGovLoading(false)
}, [model, pat, govLoading])
return (
<Ctx.Provider value={{
pat, savePat, orgs, model, issuesData,
rateLimit, loading, loadMsg, govLoading, error, totalRepo,
explore, runAudit, setError, refreshRateLimit,
}}>
{children}
</Ctx.Provider>
)
}
export const useApp = () => useContext(Ctx)