Skip to content

Commit ffaaac1

Browse files
feat: add pr metrics (#303)
1 parent 53762aa commit ffaaac1

10 files changed

Lines changed: 1408 additions & 5 deletions

File tree

.github/workflows/update-pages.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@ jobs:
4949
run: |
5050
mkdir -p gh-pages
5151
git fetch --depth=1 origin gh-pages
52-
git archive origin/gh-pages github/commitActivity | tar -x -C gh-pages
53-
if git cat-file -e origin/gh-pages:github/commitActivityHashes; then
54-
git archive origin/gh-pages github/commitActivityHashes | tar -x -C gh-pages
55-
fi
52+
for cache_path in github/commitActivity github/commitActivityHashes github/prMetrics; do
53+
if git cat-file -e "origin/gh-pages:${cache_path}"; then
54+
git archive origin/gh-pages "${cache_path}" | tar -x -C gh-pages
55+
fi
56+
done
5657
5758
- name: Collect data
5859
env:
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use strict';
2+
3+
function sortableValue(cell) {
4+
const text = cell.textContent.trim();
5+
if (!text || text === '—') return null;
6+
7+
const duration = text.match(/^(-?[\d,.]+)\s*([dh])(?:\s|$)/i);
8+
if (duration) {
9+
const hours = Number(duration[1].replaceAll(',', ''));
10+
return duration[2].toLowerCase() === 'd' ? hours * 24 : hours;
11+
}
12+
13+
const numeric = text.match(/^-?[\d,.]+(?:%|\s|$)/);
14+
if (numeric) return Number(numeric[0].replaceAll(/[,%\s]/g, ''));
15+
return text.toLocaleLowerCase();
16+
}
17+
18+
function compareSortValues(left, right, direction) {
19+
if (left === null) return right === null ? 0 : 1;
20+
if (right === null) return -1;
21+
if (typeof left === 'number' && typeof right === 'number') {
22+
return (left - right) * direction;
23+
}
24+
return String(left).localeCompare(String(right), undefined, { numeric: true }) * direction;
25+
}
26+
27+
function sortTable(table, column, direction) {
28+
const body = table.tBodies[0];
29+
if (!body) return;
30+
const rows = Array.from(body.rows).map((row, index) => ({ row, index }));
31+
rows.sort((left, right) => (
32+
compareSortValues(
33+
sortableValue(left.row.cells[column]),
34+
sortableValue(right.row.cells[column]),
35+
direction,
36+
) || left.index - right.index
37+
));
38+
rows.forEach(({ row }) => body.append(row));
39+
}
40+
41+
function resetSortIndicators(headers) {
42+
headers.forEach(header => {
43+
header.removeAttribute('aria-sort');
44+
header.querySelector('.pr-metrics-sort-indicator').textContent = '↕';
45+
});
46+
}
47+
48+
function activateSort(table, headers, header, indicator, column) {
49+
const ascending = header.getAttribute('aria-sort') !== 'ascending';
50+
resetSortIndicators(headers);
51+
header.setAttribute('aria-sort', ascending ? 'ascending' : 'descending');
52+
indicator.textContent = ascending ? '↑' : '↓';
53+
sortTable(table, column, ascending ? 1 : -1);
54+
}
55+
56+
function handleSortKey(event, activate) {
57+
if (['Enter', ' '].includes(event.key)) {
58+
event.preventDefault();
59+
activate();
60+
}
61+
}
62+
63+
function initializeHeader(table, headers, header, column) {
64+
header.tabIndex = 0;
65+
header.style.cursor = 'pointer';
66+
header.style.userSelect = 'none';
67+
header.setAttribute('role', 'button');
68+
header.setAttribute('title', 'Sort by this column');
69+
const indicator = document.createElement('span');
70+
indicator.className = 'ms-1 pr-metrics-sort-indicator';
71+
indicator.setAttribute('aria-hidden', 'true');
72+
indicator.textContent = '↕';
73+
header.append(indicator);
74+
75+
const activate = () => activateSort(table, headers, header, indicator, column);
76+
header.addEventListener('click', activate);
77+
header.addEventListener('keydown', event => handleSortKey(event, activate));
78+
}
79+
80+
function initializeTable(table) {
81+
if (table.dataset.sortableInitialized) return;
82+
table.dataset.sortableInitialized = 'true';
83+
const headers = Array.from(table.querySelectorAll('thead th'));
84+
headers.forEach((header, column) => initializeHeader(table, headers, header, column));
85+
}
86+
87+
function initializeSortableTables(root = document) {
88+
root.querySelectorAll('table.pr-metrics-sortable').forEach(table => {
89+
initializeTable(table);
90+
});
91+
}
92+
93+
document.addEventListener('DOMContentLoaded', () => initializeSortableTables());
94+
95+
/* istanbul ignore next */
96+
if (typeof module !== 'undefined' && module.exports) {
97+
module.exports = {
98+
sortableValue,
99+
compareSortValues,
100+
sortTable,
101+
initializeSortableTables,
102+
};
103+
}

gh-pages-template/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
<li class="nav-item"><a class="nav-link py-0" href="#issues">Issues</a></li>
5959
<li class="nav-item"><a class="nav-link py-0" href="#code-scanning">Code Scanning</a></li>
6060
<li class="nav-item"><a class="nav-link py-0" href="#prs">PRs</a></li>
61+
<li class="nav-item"><a class="nav-link py-0" href="{{ '/pr-metrics/' | relative_url }}">PR Metrics</a></li>
6162
<li class="nav-item"><a class="nav-link py-0" href="#license">License</a></li>
6263
<li class="nav-item"><a class="nav-link py-0" href="#coverage">Coverage</a></li>
6364
<li class="nav-item"><a class="nav-link py-0" href="#commit-activity">Commit Activity</a></li>
@@ -99,6 +100,7 @@ <h3 class="mt-4">History</h3>
99100
<!-- Pull Requests -->
100101
<section id="prs" class="mb-5">
101102
<h2>Open Pull Requests</h2>
103+
<p><a href="{{ '/pr-metrics/' | relative_url }}">View organization and repository PR metrics</a></p>
102104
<h3>By Status</h3>
103105
<div id="chart-prs" style="height:450px"></div>
104106
<h3 class="mt-4">PR Details</h3>

src/builder.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# local imports
99
from src import BASE_DIR, TEMPLATE_DIR
1010
from src import helpers
11+
from src import pr_metrics
1112
from src.logger import log
1213

1314

@@ -264,6 +265,7 @@ def build():
264265
commit_activity = []
265266
star_history = []
266267
code_scanning_history = []
268+
pr_metric_caches = {}
267269

268270
for repo in raw_repos:
269271
if repo.get('private') or repo.get('archived'):
@@ -282,6 +284,8 @@ def build():
282284
star_history.extend(_get_star_history(BASE_DIR, name))
283285
code_scanning_open = _get_code_scanning_open(BASE_DIR, name)
284286
code_scanning_history.extend(_get_code_scanning_history(BASE_DIR, name))
287+
if pr_metrics.is_active_repo(repo):
288+
pr_metric_caches[name] = pr_metrics.load_cache(BASE_DIR, name)
285289

286290
repos.append(_build_repo_entry(repo, coverage, languages, prs, issues, rtd_repos, code_scanning_open))
287291
prs_all.extend({'repo': name, **pr} for pr in prs)
@@ -301,10 +305,13 @@ def write_json(filename, data):
301305
write_json('commit_activity.json', commit_activity)
302306
write_json('star_history.json', star_history)
303307
write_json('code_scanning_history.json', code_scanning_history)
308+
write_json('pr_metrics.json', pr_metric_caches)
309+
now = datetime.now(timezone.utc)
304310
write_json('metadata.json', {
305-
'updated_at': datetime.now(timezone.utc).isoformat(),
311+
'updated_at': now.isoformat(),
306312
'repo_count': len(repos),
307313
})
314+
pr_metrics.write_report_pages(TEMPLATE_DIR, pr_metric_caches, now)
308315

309316
log.info('Dashboard build complete.')
310317

0 commit comments

Comments
 (0)