From edc97060482252e7827390182113364afce3bfc1 Mon Sep 17 00:00:00 2001 From: Ritikydv1 Date: Tue, 9 Jun 2026 09:09:34 +0530 Subject: [PATCH 1/4] Add metric info cards for overview, repository and contributor insights --- package-lock.json | 4 +- src/pages/ContributorsPage.jsx | 212 ++++++++++++++++++--- src/pages/OverviewPage.jsx | 87 +++++++-- src/pages/RepositoriesPage.jsx | 339 +++++++++++++++++++++------------ 4 files changed, 473 insertions(+), 169 deletions(-) diff --git a/package-lock.json b/package-lock.json index abd5913..29676f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "orgexplorer", - "version": "2.0.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "orgexplorer", - "version": "2.0.0", + "version": "1.0.0", "dependencies": { "@tailwindcss/vite": "^4.3.0", "d3": "^7.9.0", diff --git a/src/pages/ContributorsPage.jsx b/src/pages/ContributorsPage.jsx index d0f6370..174b77c 100644 --- a/src/pages/ContributorsPage.jsx +++ b/src/pages/ContributorsPage.jsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from 'react' +import React, { useState, useMemo, useEffect, useRef } from 'react' import { FiDatabase, FiDownload } from 'react-icons/fi' import { useApp } from '../context/AppContext' import { C, SortTh, PageTitle, LoadMore } from '../components/UI' @@ -6,21 +6,52 @@ import { useSortedData } from '../hooks/useSortedData' import { computeBusFactor, exportContributorsCSV } from '../services/analytics' import { useNavigate } from 'react-router-dom' import EmptyStateCard from '../components/EmptyStateCard' +import { BsFillInfoSquareFill } from "react-icons/bs"; export default function ContributorsPage() { const { model } = useApp() const [search, setSearch] = useState('') - const [shown, setShown] = useState(20) + const [shown, setShown] = useState(20) + const [openInfo, setOpenInfo] = useState(null) + const busFactorRef = useRef(null) + const freshnessRef = useRef(null) + const signalRef = useRef(null) + + useEffect(() => { + const handleClickOutside = (e) => { + if ( + busFactorRef.current && + busFactorRef.current.contains(e.target) + ) return + + if ( + freshnessRef.current && + freshnessRef.current.contains(e.target) + ) return + + if ( + signalRef.current && + signalRef.current.contains(e.target) + ) return + + setOpenInfo(null) + } + + document.addEventListener('mousedown', handleClickOutside) + + return () => + document.removeEventListener('mousedown', handleClickOutside) + }, []) if (!model) return null const { contributors } = model const navigate = useNavigate() - const busFactor = useMemo(() => computeBusFactor(contributors), [contributors]) - const topActive = contributors.slice(0, 10).filter(c => c.freshness > 50).length - const freshPct = contributors.length ? Math.round(topActive / Math.min(10, contributors.length) * 100) : 0 + const busFactor = useMemo(() => computeBusFactor(contributors), [contributors]) + const topActive = contributors.slice(0, 10).filter(c => c.freshness > 50).length + const freshPct = contributors.length ? Math.round(topActive / Math.min(10, contributors.length) * 100) : 0 const connectors = contributors.filter(c => c.isConnector) - const crossOrg = contributors.filter(c => c.isCrossOrg) + const crossOrg = contributors.filter(c => c.isCrossOrg) const filtered = useMemo(() => contributors.filter(c => !search || c.login.toLowerCase().includes(search.toLowerCase())), @@ -30,13 +61,13 @@ export default function ContributorsPage() { const visible = sorted.slice(0, shown) const riskColor = r => r === 'critical' ? 'var(--red)' : r === 'high' ? 'var(--amber)' : 'var(--green)' - const riskBar = r => r === 'critical' ? '90%' : r === 'high' ? '60%' : '25%' + const riskBar = r => r === 'critical' ? '90%' : r === 'high' ? '60%' : '25%' return (
exportContributorsCSV(contributors)} style={{ ...C.btn('ghost'), fontSize: 12, display: 'flex', alignItems: 'center', gap: 5 }}> Export CSV @@ -52,7 +83,42 @@ export default function ContributorsPage() { ...C.card, borderColor: busFactor.risk === 'critical' ? 'rgba(239,68,68,.4)' : busFactor.risk === 'high' ? 'rgba(245,158,11,.4)' : 'var(--border)', }}> -
Bus Factor Risk
+
+

Bus Factor Risk

+ + + + {openInfo === 'busfactor' && ( +
+
+

Bus Factor

+ +

Measures contributor concentration risk.

+ +
    +
  • 1 = Critical Risk
  • +
  • 2 = High Risk
  • +
  • 3+ = Healthy Distribution
  • +
+
+ +

+ Higher values indicate knowledge is distributed across more contributors, + reducing dependency on a small number of individuals. +

+
+ )} +
Bus Factor: {busFactor.factor}
-
Freshness Index
+
+

Freshness Index

+ + + + {openInfo === 'freshness' && ( +
+
+

Freshness Index

+ +

+ Measures how active and recently engaged the contributor community is. +

+ +
    +
  • High Score = Contributors active recently
  • +
  • Medium Score = Some recent activity
  • +
  • Low Score = Limited recent participation
  • +
+
+ +

+ Higher values indicate stronger project momentum and ongoing maintenance. +

+
+ )} +
- {filtered.length} contributors — no rank column by design + {filtered.length} contributors found
{contributors?.length ? @@ -136,7 +248,59 @@ export default function ContributorsPage() { - SIGNALS + +
+

SIGNALS

+ + + + {openInfo === 'signals' && ( +
+

Contributor Signals

+ +
+

+ Measures how contributors connect repositories and organizations. +

+ +
    +
  • + Connector Contributors — active in 3+ repositories. +
  • +
  • + Cross-Org Contributors — contribute across multiple organizations. +
  • +
+ +

+ Higher values indicate stronger collaboration and knowledge sharing. +

+
+ )} +
+ @@ -174,18 +338,18 @@ export default function ContributorsPage() { setShown(s => s + 20)} />) : (<>
- } - title="No contributors found" - description="We couldn't find any contributor data for this organization. " - buttonText="Go to Home" - onButtonClick={() => navigate('/')}/> + style={{ + padding: '32px 24px', + maxWidth: 900, + margin: '0 auto', + }} + > + } + title="No contributors found" + description="We couldn't find any contributor data for this organization. " + buttonText="Go to Home" + onButtonClick={() => navigate('/')} />
)}
diff --git a/src/pages/OverviewPage.jsx b/src/pages/OverviewPage.jsx index 5460f95..c743007 100644 --- a/src/pages/OverviewPage.jsx +++ b/src/pages/OverviewPage.jsx @@ -1,10 +1,11 @@ -import React from 'react' +import React, { useEffect, useState, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { FiExternalLink, FiShare2, FiArrowRight } from 'react-icons/fi' import { useApp } from '../context/AppContext' import { C, StatCard, HealthBar } from '../components/UI' +import { BsFillInfoSquareFill } from "react-icons/bs"; -const LANG_COLORS = ['#22c55e','#f5c518','#3b82f6','#ef4444','#a855f7','#f97316','#06b6d4'] +const LANG_COLORS = ['#22c55e', '#f5c518', '#3b82f6', '#ef4444', '#a855f7', '#f97316', '#06b6d4'] const fmt = n => n > 999 ? (n / 1000).toFixed(1) + 'k' : String(n) export default function OverviewPage() { @@ -12,15 +13,30 @@ export default function OverviewPage() { const navigate = useNavigate() if (!model) return null + const [open, setOpen] = useState(false) + const infoRef = useRef(null) + + useEffect(() => { + function handleClickOutside(event) { + if (infoRef.current && !infoRef.current.contains(event.target)) { + setOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + const { allRepos } = model - const isMulti = orgs.length > 1 - const totalStars = allRepos.reduce((s, r) => s + r.stargazers_count, 0) - const totalForks = allRepos.reduce((s, r) => s + r.forks_count, 0) + const isMulti = orgs.length > 1 + const totalStars = allRepos.reduce((s, r) => s + r.stargazers_count, 0) + const totalForks = allRepos.reduce((s, r) => s + r.forks_count, 0) const activeRepos = allRepos.filter(r => r.lifecycle === 'Thriving' || r.lifecycle === 'Stable').length const langMap = {} allRepos.forEach(r => { if (r.language) langMap[r.language] = (langMap[r.language] || 0) + 1 }) - const langs = Object.entries(langMap).sort((a, b) => b[1] - a[1]).slice(0, 7) + const langs = Object.entries(langMap).sort((a, b) => b[1] - a[1]).slice(0, 7) const langTotal = langs.reduce((s, [, c]) => s + c, 0) const topRepos = [...allRepos].sort((a, b) => b.healthScore - a.healthScore).slice(0, 5) @@ -85,9 +101,9 @@ export default function OverviewPage() { {/* Stats */}
- - - + + +
@@ -113,15 +129,50 @@ export default function OverviewPage() {
-
-
High Impact Repositories
+
+
+

High Impact Repositories

+ + +
+ + {open && ( +
+ Health Score estimates the overall health of a repository on a scale of 0 – 100. + +
    +
  • + Activity (40%) – How recently the repository has been updated. +
  • +
  • + Issue Health (30%) – Balance between open issues and maintenance. +
  • +
  • + Contributor Diversity (30%) – Number of active contributors. +
  • +
+ +

+ Higher scores indicate healthier and more actively maintained repositories. +

+
+ )}
By Composite Health Score
{topRepos.map(r => (
{r.name} - {r.healthScore}
@@ -132,12 +183,12 @@ export default function OverviewPage() { {/* Nav cards */}
- - - - - - + + + + + +
) diff --git a/src/pages/RepositoriesPage.jsx b/src/pages/RepositoriesPage.jsx index 3f87597..a77eb4f 100644 --- a/src/pages/RepositoriesPage.jsx +++ b/src/pages/RepositoriesPage.jsx @@ -1,5 +1,6 @@ -import React, { useState, useMemo } from 'react' +import React, { useState, useMemo, useEffect, useRef } from 'react' import { FiDatabase, FiDownload, FiGrid, FiList } from 'react-icons/fi' +import { BsFillInfoSquareFill } from "react-icons/bs"; import { useApp } from '../context/AppContext' import { C, Badge, HealthBar, SortTh, PageTitle, LoadMore } from '../components/UI' import { useSortedData } from '../hooks/useSortedData' @@ -7,16 +8,33 @@ import { exportReposCSV } from '../services/analytics' import EmptyStateCard from '../components/EmptyStateCard' import { useNavigate } from 'react-router-dom' -const LIFECYCLES = ['All','Thriving','Stable','Dormant','Abandoned'] -const LC_ACTIVE = { Thriving:'var(--green)', Stable:'var(--blue)', Dormant:'var(--amber)', Abandoned:'var(--red)' } +const LIFECYCLES = ['All', 'Thriving', 'Stable', 'Dormant', 'Abandoned'] +const LC_ACTIVE = { Thriving: 'var(--green)', Stable: 'var(--blue)', Dormant: 'var(--amber)', Abandoned: 'var(--red)' } export default function RepositoriesPage() { const { model } = useApp() - const [search, setSearch] = useState('') + const [search, setSearch] = useState('') const [lifecycle, setLifecycle] = useState('All') - const [lang, setLang] = useState('All') - const [view, setView] = useState('grid') - const [shown, setShown] = useState(20) + const [lang, setLang] = useState('All') + const [view, setView] = useState('grid') + const [shown, setShown] = useState(20) + const [openInfo, setOpenInfo] = useState(false) + const infoRef = useRef(null) + + useEffect(() => { + function handleClickOutside(event) { + if ( + infoRef.current && + !infoRef.current.contains(event.target) + ) { + setOpenInfo(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) const navigate = useNavigate() if (!model) return null @@ -37,27 +55,98 @@ export default function RepositoriesPage() { const visible = sorted.slice(0, shown) const TABLE_COLS = [ - ['name', 'Repository'], + ['name', 'Repository'], ['stargazers_count', 'Stars'], - ['forks_count', 'Forks'], - ['open_issues_count','Open Issues'], - ['healthScore', 'Health'], - ['lifecycle', 'Lifecycle'], - ['pushed_at', 'Last Push'], + ['forks_count', 'Forks'], + ['open_issues_count', 'Open Issues'], + ['healthScore', 'Health'], + ['lifecycle', 'Lifecycle'], + ['pushed_at', 'Last Push'], ] return (
- - {filtered.length} - / {allRepos.length} repos - - } - /> +
+ + Repository Explorer + + +
+ } + subtitle="Technical health and lifecycle across all repositories in the portfolio" + right={ + + {filtered.length} + + {' '} / {allRepos.length} repos + + + } + /> + + {openInfo && ( +
+
+ Repository Health Metrics +
+ +

+ OrgExplorer evaluates repositories using activity, issue health, + contributor diversity, and lifecycle status. +

+ +
+ Health Score (0–100) +
    +
  • Activity → 40%
  • +
  • Issue Health → 30%
  • +
  • Contributor Diversity → 30%
  • +
+ + Lifecycle Classification +
    +
  • 🟢 Thriving → Updated within 30 days
  • +
  • 🔵 Stable → Updated within 90 days
  • +
  • 🟡 Dormant → Updated within 180 days
  • +
  • 🔴 Abandoned → No updates for 180+ days
  • +
+ + Repository Signals +
    +
  • Stars → Community interest
  • +
  • Forks → Adoption & contributions
  • +
  • Issues → Current maintenance workload
  • +
+
+
+ )} +
{/* Filter bar */}
@@ -98,113 +187,113 @@ export default function RepositoriesPage() { ))}
-{allRepos?.length ? ( - <> - {/* Table view */} - {view === 'list' && ( -
- - - - {TABLE_COLS.map(([k, l]) => ( - - ))} - - - - {visible.map((r, i) => ( - - - - - - - - - - ))} - -
-
{r.name}
- {r.orgLogin &&
{r.orgLogin}
} -
{r.stargazers_count.toLocaleString()}{r.forks_count.toLocaleString()} 30 ? 'var(--red)' : 'var(--text2)' }}>{r.open_issues_count}{r.pushed_at?.slice(0, 10)}
- setShown(s => s + 20)} /> -
- )} - - {/* Grid view */} - {view === 'grid' && ( + {allRepos?.length ? ( <> -
- {visible.map(r => ( -
e.currentTarget.style.borderColor = 'var(--accent)'} - onMouseLeave={e => e.currentTarget.style.borderColor = - r.lifecycle === 'Thriving' ? 'rgba(34,197,94,.25)' : - r.lifecycle === 'Abandoned' ? 'rgba(239,68,68,.25)' : 'var(--border)'} - style={{ - ...C.card, - borderColor: r.lifecycle === 'Thriving' ? 'rgba(34,197,94,.25)' : r.lifecycle === 'Abandoned' ? 'rgba(239,68,68,.25)' : 'var(--border)', - transition: 'border-color .2s', display: 'flex', flexDirection: 'column', gap: 10, - }} - > -
-
-
{r.name}
- {r.orgLogin &&
{r.orgLogin}
} -
- -
-

- {r.description || 'No description provided'} -

-
- {[['Stars', r.stargazers_count.toLocaleString()], ['Forks', r.forks_count.toLocaleString()], ['Issues', r.open_issues_count]].map(([l, v]) => ( -
-
{v}
-
{l}
-
+ {/* Table view */} + {view === 'list' && ( +
+ + + + {TABLE_COLS.map(([k, l]) => ( + + ))} + + + + {visible.map((r, i) => ( + + + + + + + + + ))} - - {r.language && ( -
- - {r.language} -
- )} -
-
- HEALTH SCORE - {r.pushed_at?.slice(0, 10)} +
+
+
{r.name}
+ {r.orgLogin &&
{r.orgLogin}
} +
{r.stargazers_count.toLocaleString()}{r.forks_count.toLocaleString()} 30 ? 'var(--red)' : 'var(--text2)' }}>{r.open_issues_count}{r.pushed_at?.slice(0, 10)}
+ setShown(s => s + 20)} /> +
+ )} + + {/* Grid view */} + {view === 'grid' && ( + <> +
+ {visible.map(r => ( +
e.currentTarget.style.borderColor = 'var(--accent)'} + onMouseLeave={e => e.currentTarget.style.borderColor = + r.lifecycle === 'Thriving' ? 'rgba(34,197,94,.25)' : + r.lifecycle === 'Abandoned' ? 'rgba(239,68,68,.25)' : 'var(--border)'} + style={{ + ...C.card, + borderColor: r.lifecycle === 'Thriving' ? 'rgba(34,197,94,.25)' : r.lifecycle === 'Abandoned' ? 'rgba(239,68,68,.25)' : 'var(--border)', + transition: 'border-color .2s', display: 'flex', flexDirection: 'column', gap: 10, + }} + > +
+
+
{r.name}
+ {r.orgLogin &&
{r.orgLogin}
} +
+ +
+

+ {r.description || 'No description provided'} +

+
+ {[['Stars', r.stargazers_count.toLocaleString()], ['Forks', r.forks_count.toLocaleString()], ['Issues', r.open_issues_count]].map(([l, v]) => ( +
+
{v}
+
{l}
+
+ ))} +
+ {r.language && ( +
+ + {r.language} +
+ )} +
+
+ HEALTH SCORE + {r.pushed_at?.slice(0, 10)} +
+ +
+
ACTIVITY 40% · ISSUES 30% · DIVERSITY 30%
- -
-
ACTIVITY 40% · ISSUES 30% · DIVERSITY 30%
+ ))}
- ))} -
- setShown(s => s + 20)} /> - + setShown(s => s + 20)} /> + )} - ) - : ( -
- } - title="No repositories available" - description="We couldn't find any repositories for this organization yet." - buttonText="Go to Home" - onButtonClick={() => navigate('/')} - /> -
- )} + ) + : ( +
+ } + title="No repositories available" + description="We couldn't find any repositories for this organization yet." + buttonText="Go to Home" + onButtonClick={() => navigate('/')} + /> +
+ )}
) } From c507a22f571e52f9d5205338bfd5ae1437178f4c Mon Sep 17 00:00:00 2001 From: Ritikydv1 Date: Wed, 10 Jun 2026 18:11:03 +0530 Subject: [PATCH 2/4] fix: resolve invalid table header structure --- src/pages/ContributorsPage.jsx | 101 ++++++++++++++++----------------- 1 file changed, 50 insertions(+), 51 deletions(-) diff --git a/src/pages/ContributorsPage.jsx b/src/pages/ContributorsPage.jsx index 174b77c..1b74eee 100644 --- a/src/pages/ContributorsPage.jsx +++ b/src/pages/ContributorsPage.jsx @@ -247,60 +247,59 @@ export default function ContributorsPage() { - - */} + +
-
-

SIGNALS

+

SIGNALS

- + + {openInfo === 'signals' && ( +
- - - - {openInfo === 'signals' && ( -
-

Contributor Signals

- -
-

- Measures how contributors connect repositories and organizations. -

- -
    -
  • - Connector Contributors — active in 3+ repositories. -
  • -
  • - Cross-Org Contributors — contribute across multiple organizations. -
  • -
- -

- Higher values indicate stronger collaboration and knowledge sharing. +

Contributor Signals

+ +
+

+ Measures how contributors connect repositories and organizations.

-
- )} -
- + +
    +
  • + Connector Contributors — active in 3+ repositories. +
  • +
  • + Cross-Org Contributors — contribute across multiple organizations. +
  • +
+ +

+ Higher values indicate stronger collaboration and knowledge sharing. +

+
+ )} +
@@ -353,6 +352,6 @@ export default function ContributorsPage() {
)} - + ) } From 8461629e1d11c9ee2254990f2ddffaad64322bcb Mon Sep 17 00:00:00 2001 From: Ritikydv1 Date: Wed, 10 Jun 2026 18:12:00 +0530 Subject: [PATCH 3/4] fix: resolve invalid table header structure --- src/pages/ContributorsPage.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/ContributorsPage.jsx b/src/pages/ContributorsPage.jsx index 1b74eee..757571b 100644 --- a/src/pages/ContributorsPage.jsx +++ b/src/pages/ContributorsPage.jsx @@ -247,7 +247,6 @@ export default function ContributorsPage() { - {/* */} Date: Wed, 10 Jun 2026 18:37:53 +0530 Subject: [PATCH 4/4] feat(settings): add PAT security info tooltip --- src/pages/SettingsPage.jsx | 89 ++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 9 deletions(-) diff --git a/src/pages/SettingsPage.jsx b/src/pages/SettingsPage.jsx index 41c840b..be74441 100644 --- a/src/pages/SettingsPage.jsx +++ b/src/pages/SettingsPage.jsx @@ -1,14 +1,15 @@ -import React, { useState } from 'react' +import React, { useState, useEffect, useRef } from 'react' import { FiEye, FiEyeOff, FiTrash2, FiSave, FiRefreshCw } from 'react-icons/fi' import { useApp } from '../context/AppContext' import { C } from '../components/UI' import { cacheClear } from '../services/github' +import { BsFillInfoSquareFill } from "react-icons/bs"; export default function SettingsPage() { const { pat, savePat, rateLimit } = useApp() - const [draft, setDraft] = useState(pat) - const [show, setShow] = useState(false) - const [saved, setSaved] = useState(false) + const [draft, setDraft] = useState(pat) + const [show, setShow] = useState(false) + const [saved, setSaved] = useState(false) const [cleared, setCleared] = useState(false) const handleSave = () => { @@ -32,6 +33,21 @@ export default function SettingsPage() { ? rateLimit.remaining / rateLimit.limit > 0.3 ? 'var(--green)' : 'var(--red)' : 'var(--text2)' + const [open, setOpen] = useState(false) + const infoRef = useRef(null) + + useEffect(() => { + function handleClickOutside(event) { + if (infoRef.current && !infoRef.current.contains(event.target)) { + setOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + return (

Settings

@@ -45,7 +61,62 @@ export default function SettingsPage() {
-
GitHub Authentication
+
+

GitHub Authentication

+ + + + {open && ( +
+

+ PAT Security +

+ +
+

+ Your GitHub Personal Access Token (PAT) is stored locally on your + device and is never sent to OrgExplorer servers. +

+ +
    +
  • Stored only in your browser.
  • +
  • Used exclusively for GitHub API authentication.
  • +
  • Never shared with third parties.
  • +
  • Can be removed at any time from Settings.
  • +
+ +

+ Using a PAT increases GitHub API limits and allows access to + repositories you are authorized to view. +

+
+
+ )} +
Personal Access Token (PAT)
{pat && ( @@ -171,10 +242,10 @@ export default function SettingsPage() {
Architect Meta
{[ - ['Core Version', 'v2.0.0-stable'], - ['Architecture', 'Client-side only, no backend'], - ['API strategy', '53 req/hr unauthenticated'], - ['Cache', 'IndexedDB + React Context'], + ['Core Version', 'v2.0.0-stable'], + ['Architecture', 'Client-side only, no backend'], + ['API strategy', '53 req/hr unauthenticated'], + ['Cache', 'IndexedDB + React Context'], ].map(([k, v]) => (
{k}