harshapitla/github-actions-analytics
0
1import React, { useMemo } from "react";2import "./overview.css";3 4/* ================= AUTHOR EXTRACTOR ================= */5function extractAuthor(row) {6 if (!row.author_profile_link) return "missing_author";7 8 try {9 const url = new URL(row.author_profile_link);10 const parts = url.pathname.split("/").filter(Boolean);11 return parts[0] || "missing_author";12 } catch {13 return "missing_author";14 }15}16 17/* ================= KPI CARD ================= */18function KpiCard({ icon, title, value, subtitle, accent }) {19 return (20 <div className={`kpi-card ${accent || ""}`}>21 <div className="kpi-header">22 <span className="kpi-icon">{icon}</span>23 <span className="kpi-title">{title}</span>24 </div>25 <div className="kpi-value">{value}</div>26 {subtitle && <div className="kpi-sub">{subtitle}</div>}27 </div>28 );29}30 31/* ================= PROGRESS ROW ================= */32function ProgressRow({ index, name, value, max, unit }) {33 const pct = Math.min((value / max) * 100, 100);34 35 return (36 <div className="progress-row">37 <div className="progress-top">38 <span className="progress-rank">{index}.</span>39 <span className="progress-name" title={name}>40 {name}41 </span>42 <span className="progress-value">43 {value.toLocaleString()} {unit}44 </span>45 </div>46 47 <div className="progress-bar">48 <div className="progress-fill" style={{ width: `${pct}%` }} />49 </div>50 </div>51 );52}53 54/* ================= RANK CARD ================= */55function RankedCard({ title, subtitle, items, unit }) {56 const max = Math.max(...items.map((i) => i.value), 1);57 58 return (59 <div className="rank-card">60 <div className="rank-header">61 <div>62 <h3>{title}</h3>63 <p>{subtitle}</p>64 </div>65 </div>66 67 {items.map((item, i) => (68 <ProgressRow69 key={i}70 index={i + 1}71 name={item.name}72 value={item.value}73 max={max}74 unit={unit}75 />76 ))}77 </div>78 );79}80 81/* ================= MAIN OVERVIEW ================= */82export default function KpiOverview({ rows = [] }) {83 const metrics = useMemo(() => {84 if (!rows.length) return null;85 86 let totalStars = 0;87 let missingAuthorCount = 0;88 let withVersionCount = 0;89 90 const authors = new Map();91 const tags = new Map();92 93 rows.forEach((r) => {94 const stars = Number(r.no_of_stars) || 0;95 totalStars += stars;96 97 // Author98 const author = extractAuthor(r);99 if (author === "missing_author") {100 missingAuthorCount += 1;101 } else {102 authors.set(author, (authors.get(author) || 0) + stars);103 }104 105 // Version presence106 if (r.latest_version && String(r.latest_version).trim() !== "") {107 withVersionCount += 1;108 }109 110 // Tags111 if (r.tags) {112 r.tags.split(",").forEach((t) => {113 const tag = t.trim().toLowerCase();114 if (tag) tags.set(tag, (tags.get(tag) || 0) + 1);115 });116 }117 });118 119 // Top 10% star share120 const sortedStars = rows121 .map((r) => Number(r.no_of_stars) || 0)122 .sort((a, b) => b - a);123 124 const top10Count = Math.ceil(sortedStars.length * 0.1);125 const top10Stars = sortedStars126 .slice(0, top10Count)127 .reduce((a, b) => a + b, 0);128 129 return {130 totalActions: rows.length,131 totalStars,132 avgStars: totalStars / rows.length,133 top10Share: (top10Stars / totalStars) * 100,134 135 uniqueAuthors: authors.size,136 missingAuthorPct: (missingAuthorCount / rows.length) * 100,137 versionCoveragePct: (withVersionCount / rows.length) * 100,138 139 topAuthors: [...authors.entries()]140 .sort((a, b) => b[1] - a[1])141 .slice(0, 3)142 .map(([name, value]) => ({ name, value })),143 144 topActions: [...rows]145 .filter((r) => r.no_of_stars > 0)146 .sort((a, b) => b.no_of_stars - a.no_of_stars)147 .slice(0, 3)148 .map((r) => ({149 name: r.action_title,150 value: r.no_of_stars,151 })),152 153 topTags: [...tags.entries()]154 .sort((a, b) => b[1] - a[1])155 .slice(0, 3)156 .map(([name, value]) => ({ name, value })),157 };158 }, [rows]);159 160 if (!metrics) return null;161 162 return (163 <>164 {/* ================= CONTEXT ================= */}165 <div className="snapshot-banner">166 <strong>Data snapshot</strong> โ based on latest available action167 metadata168 </div>169 170 {/* ================= KPI GRID ================= */}171 <div className="kpi-grid">172 <KpiCard icon="๐ฆ" title="Total Actions" value={metrics.totalActions} />173 174 <KpiCard175 icon="๐ค"176 title="Unique Authors"177 value={metrics.uniqueAuthors}178 subtitle="Publishers with valid profiles"179 />180 181 <KpiCard182 icon="โญ"183 title="Total Stars"184 value={metrics.totalStars.toLocaleString()}185 />186 187 <KpiCard188 icon="๐"189 title="Avg Stars per Action"190 value={metrics.avgStars.toFixed(1)}191 />192 193 <KpiCard194 icon="๐ฅ"195 title="Top 10% Star Share"196 value={`${metrics.top10Share.toFixed(1)}%`}197 subtitle="Star concentration"198 accent="accent-warning"199 />200 201 <KpiCard202 icon="๐ฆ"203 title="Version Coverage"204 value={`${metrics.versionCoveragePct.toFixed(1)}%`}205 subtitle="Actions with a version listed"206 />207 208 <KpiCard209 icon="โ ๏ธ"210 title="Missing Author Info"211 value={`${metrics.missingAuthorPct.toFixed(1)}%`}212 subtitle="Actions without author profile"213 />214 </div>215 216 {/* ================= RANKED SECTIONS ================= */}217 <div className="rank-grid">218 <RankedCard219 title="Top Authors"220 subtitle="Publishers with highest total stars"221 items={metrics.topAuthors}222 unit="stars"223 />224 225 <RankedCard226 title="Top Actions"227 subtitle="Most starred actions"228 items={metrics.topActions}229 unit="stars"230 />231 232 <RankedCard233 title="Top Tags"234 subtitle="Most commonly used categories"235 items={metrics.topTags}236 unit="actions"237 />238 </div>239 </>240 );241}242 