Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/update-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ jobs:
run: |
mkdir -p gh-pages
git fetch --depth=1 origin gh-pages
git archive origin/gh-pages github/commitActivity | tar -x -C gh-pages
if git cat-file -e origin/gh-pages:github/commitActivityHashes; then
git archive origin/gh-pages github/commitActivityHashes | tar -x -C gh-pages
fi
for cache_path in github/commitActivity github/commitActivityHashes github/prMetrics; do
if git cat-file -e "origin/gh-pages:${cache_path}"; then
git archive origin/gh-pages "${cache_path}" | tar -x -C gh-pages
fi
done

- name: Collect data
env:
Expand Down
103 changes: 103 additions & 0 deletions gh-pages-template/assets/js/pr-metrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'use strict';

function sortableValue(cell) {
const text = cell.textContent.trim();
if (!text || text === '—') return null;

const duration = text.match(/^(-?[\d,.]+)\s*([dh])(?:\s|$)/i);
if (duration) {
const hours = Number(duration[1].replaceAll(',', ''));
return duration[2].toLowerCase() === 'd' ? hours * 24 : hours;
}

const numeric = text.match(/^-?[\d,.]+(?:%|\s|$)/);
if (numeric) return Number(numeric[0].replaceAll(/[,%\s]/g, ''));
return text.toLocaleLowerCase();
}

function compareSortValues(left, right, direction) {
if (left === null) return right === null ? 0 : 1;
if (right === null) return -1;
if (typeof left === 'number' && typeof right === 'number') {
return (left - right) * direction;
}
return String(left).localeCompare(String(right), undefined, { numeric: true }) * direction;
}

function sortTable(table, column, direction) {
const body = table.tBodies[0];
if (!body) return;
const rows = Array.from(body.rows).map((row, index) => ({ row, index }));
rows.sort((left, right) => (
compareSortValues(
sortableValue(left.row.cells[column]),
sortableValue(right.row.cells[column]),
direction,
) || left.index - right.index
));
rows.forEach(({ row }) => body.append(row));
}

function resetSortIndicators(headers) {
headers.forEach(header => {
header.removeAttribute('aria-sort');
header.querySelector('.pr-metrics-sort-indicator').textContent = '↕';
});
}

function activateSort(table, headers, header, indicator, column) {
const ascending = header.getAttribute('aria-sort') !== 'ascending';
resetSortIndicators(headers);
header.setAttribute('aria-sort', ascending ? 'ascending' : 'descending');
indicator.textContent = ascending ? '↑' : '↓';
sortTable(table, column, ascending ? 1 : -1);
}

function handleSortKey(event, activate) {
if (['Enter', ' '].includes(event.key)) {
event.preventDefault();
activate();
}
}

function initializeHeader(table, headers, header, column) {
header.tabIndex = 0;
header.style.cursor = 'pointer';
header.style.userSelect = 'none';
header.setAttribute('role', 'button');
header.setAttribute('title', 'Sort by this column');
const indicator = document.createElement('span');
indicator.className = 'ms-1 pr-metrics-sort-indicator';
indicator.setAttribute('aria-hidden', 'true');
indicator.textContent = '↕';
header.append(indicator);

const activate = () => activateSort(table, headers, header, indicator, column);
header.addEventListener('click', activate);
header.addEventListener('keydown', event => handleSortKey(event, activate));
}

function initializeTable(table) {
if (table.dataset.sortableInitialized) return;
table.dataset.sortableInitialized = 'true';
const headers = Array.from(table.querySelectorAll('thead th'));
headers.forEach((header, column) => initializeHeader(table, headers, header, column));
}

function initializeSortableTables(root = document) {
root.querySelectorAll('table.pr-metrics-sortable').forEach(table => {
initializeTable(table);
});
}

document.addEventListener('DOMContentLoaded', () => initializeSortableTables());

/* istanbul ignore next */
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
sortableValue,
compareSortValues,
sortTable,
initializeSortableTables,
};
}
2 changes: 2 additions & 0 deletions gh-pages-template/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
<li class="nav-item"><a class="nav-link py-0" href="#issues">Issues</a></li>
<li class="nav-item"><a class="nav-link py-0" href="#code-scanning">Code Scanning</a></li>
<li class="nav-item"><a class="nav-link py-0" href="#prs">PRs</a></li>
<li class="nav-item"><a class="nav-link py-0" href="{{ '/pr-metrics/' | relative_url }}">PR Metrics</a></li>
<li class="nav-item"><a class="nav-link py-0" href="#license">License</a></li>
<li class="nav-item"><a class="nav-link py-0" href="#coverage">Coverage</a></li>
<li class="nav-item"><a class="nav-link py-0" href="#commit-activity">Commit Activity</a></li>
Expand Down Expand Up @@ -99,6 +100,7 @@ <h3 class="mt-4">History</h3>
<!-- Pull Requests -->
<section id="prs" class="mb-5">
<h2>Open Pull Requests</h2>
<p><a href="{{ '/pr-metrics/' | relative_url }}">View organization and repository PR metrics</a></p>
<h3>By Status</h3>
<div id="chart-prs" style="height:450px"></div>
<h3 class="mt-4">PR Details</h3>
Expand Down
9 changes: 8 additions & 1 deletion src/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# local imports
from src import BASE_DIR, TEMPLATE_DIR
from src import helpers
from src import pr_metrics
from src.logger import log


Expand Down Expand Up @@ -264,6 +265,7 @@ def build():
commit_activity = []
star_history = []
code_scanning_history = []
pr_metric_caches = {}

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

repos.append(_build_repo_entry(repo, coverage, languages, prs, issues, rtd_repos, code_scanning_open))
prs_all.extend({'repo': name, **pr} for pr in prs)
Expand All @@ -301,10 +305,13 @@ def write_json(filename, data):
write_json('commit_activity.json', commit_activity)
write_json('star_history.json', star_history)
write_json('code_scanning_history.json', code_scanning_history)
write_json('pr_metrics.json', pr_metric_caches)
now = datetime.now(timezone.utc)
write_json('metadata.json', {
'updated_at': datetime.now(timezone.utc).isoformat(),
'updated_at': now.isoformat(),
'repo_count': len(repos),
})
pr_metrics.write_report_pages(TEMPLATE_DIR, pr_metric_caches, now)

log.info('Dashboard build complete.')

Expand Down
Loading