diff --git a/generate-dashboard.js b/generate-dashboard.js index 0449988..c91e826 100644 --- a/generate-dashboard.js +++ b/generate-dashboard.js @@ -7,6 +7,7 @@ import { generateWorkflowRunsSectionFromData, generateMissingMirrorsSectionFromData, generateSummarySection, + generateRecentActivitySection, generateHTML } from './src/html-generators.js'; @@ -51,6 +52,7 @@ async function main() { const stats = computeStats(orgDataMap, config.staleThresholds); const summarySection = generateSummarySection(stats, config); + const recentActivitySection = generateRecentActivitySection(orgDataMap); let orgSections = ''; for (const [orgName, data] of Object.entries(orgDataMap)) { @@ -60,7 +62,7 @@ async function main() { const missingMirrorsSection = generateMissingMirrorsSectionFromData(missingMirrors); const workflowSection = generateWorkflowRunsSectionFromData(reposWithRuns); - const html = generateHTML(summarySection, orgSections, missingMirrorsSection, workflowSection); + const html = generateHTML(summarySection, orgSections, missingMirrorsSection, workflowSection, recentActivitySection); await mkdir('dist', { recursive: true }); await writeFile('dist/index.html', html); diff --git a/src/html-generators.js b/src/html-generators.js index ee54d16..b076c9f 100644 --- a/src/html-generators.js +++ b/src/html-generators.js @@ -33,6 +33,35 @@ export function getWorkflowStatusIcon(conclusion) { return icons[conclusion] || ''; } +function generateIssueRow(issue, config) { + return ` + + + ${escapeHtml(issue.title)} + ${formatAge(issue.updatedAt)} + ${issue.labels.nodes.length > 0 ? ` +
+ ${issue.labels.nodes.map(label => `${escapeHtml(label.name)}`).join('')} +
+ ` : ''} + + + `; +} + +function generatePullRequestRow(pr, config) { + return ` + + + ${escapeHtml(pr.title)} + ${pr.author ? `by ${escapeHtml(pr.author.login)}` : ''} + ${formatAge(pr.updatedAt)} + ${getReviewStatusBadge(pr)} + + + `; +} + export function generateOrgSection(orgName, data, config) { const repos = data.data.organization.repositories.nodes; @@ -54,57 +83,111 @@ export function generateOrgSection(orgName, data, config) {
-
${activeRepos.map(repo => ` -
-
+
+
- ${repo.issues.totalCount > 0 ? ` -

Issues

- - - ${repo.issues.nodes.map(issue => ` - - - - `).join('')} - -
- ${escapeHtml(issue.title)} - ${formatAge(issue.updatedAt)} - ${issue.labels.nodes.length > 0 ? ` -
- ${issue.labels.nodes.map(label => `${escapeHtml(label.name)}`).join('')} -
- ` : ''} -
- ` : ''} - - ${repo.pullRequests.totalCount > 0 ? ` -

Pull Requests

- - - ${repo.pullRequests.nodes.map(pr => ` - - - - `).join('')} - -
- ${escapeHtml(pr.title)} - ${pr.author ? `by ${escapeHtml(pr.author.login)}` : ''} - ${formatAge(pr.updatedAt)} - ${getReviewStatusBadge(pr)} -
- ` : ''} +
+
+ ${repo.issues.totalCount > 0 ? ` +

Issues

+ + + ${repo.issues.nodes.map(issue => generateIssueRow(issue, config)).join('')} + +
+ ` : '
No open issues
'} +
+
+ ${repo.pullRequests.totalCount > 0 ? ` +

Pull Requests

+ + + ${repo.pullRequests.nodes.map(pr => generatePullRequestRow(pr, config)).join('')} + +
+ ` : '
No open pull requests
'} +
+
`).join('')}
+ + `; +} + +export function generateRecentActivitySection(orgDataMap, limit = 15) { + const items = []; + for (const [orgName, data] of Object.entries(orgDataMap)) { + const repos = data.data.organization.repositories.nodes; + for (const repo of repos) { + for (const issue of repo.issues.nodes) { + items.push({ + type: 'issue', + title: issue.title, + url: issue.url, + updatedAt: issue.updatedAt, + repo: repo.name, + org: orgName, + author: null + }); + } + for (const pr of repo.pullRequests.nodes) { + items.push({ + type: 'pr', + title: pr.title, + url: pr.url, + updatedAt: pr.updatedAt, + repo: repo.name, + org: orgName, + author: pr.author?.login || null + }); + } + } + } + + items.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); + const recent = items.slice(0, limit); + + if (recent.length === 0) { + return ''; + } + + const rows = recent.map(item => { + const typeBadge = item.type === 'pr' + ? 'PR' + : 'Issue'; + const author = item.author ? `by ${escapeHtml(item.author)}` : ''; + return ` + + ${typeBadge} + ${escapeHtml(item.org)}/${escapeHtml(item.repo)} + + ${escapeHtml(item.title)} + ${author} + + ${formatAge(item.updatedAt)} + + `; + }).join(''); + + return ` +
+

+ + Recent Activity + + +

+
+ + ${rows} +
`; @@ -231,7 +314,7 @@ export function generateSummarySection(stats, config) { `; } -export function generateHTML(summarySection, orgSections, missingMirrorsSection, workflowSection) { +export function generateHTML(summarySection, orgSections, missingMirrorsSection, workflowSection, recentActivitySection = '') { const lastUpdate = formatDateUTC(new Date().toISOString()); return ` @@ -249,10 +332,61 @@ export function generateHTML(summarySection, orgSections, missingMirrorsSection, padding: 1rem; } - .two-columns { + .repo-card { max-width: 1400px; - columns: 2; - column-gap: 1.5rem; + } + + .repo-split > .repo-col + .repo-col { + border-top: 1px solid rgba(0, 0, 0, 0.125); + } + + @media (min-width: 992px) { + .repo-split > .repo-col + .repo-col { + border-top: none; + border-left: 1px solid rgba(0, 0, 0, 0.125); + } + } + + .repo-col .table-title { + background-color: rgba(0, 0, 0, 0.02); + } + + .empty-col { + padding: 1rem; + font-style: italic; + } + + .activity-type { + display: inline-block; + padding: 0.1rem 0.4rem; + border-radius: 0.25rem; + font-size: 0.7em; + font-weight: 500; + min-width: 2.5rem; + text-align: center; + } + .activity-type-issue { background-color: #d1ecf1; color: #0c5460; } + .activity-type-pr { background-color: #e2d9f3; color: #3d2a6c; } + + .recent-activity-table { + max-width: 1400px; + font-size: 0.9rem; + } + .recent-activity-table td { + vertical-align: middle; + } + .recent-activity-table .activity-type-cell { + width: 3.5rem; + } + .recent-activity-table .activity-repo { + white-space: nowrap; + font-family: monospace; + font-size: 0.85em; + color: #495057; + } + .recent-activity-table .activity-age { + white-space: nowrap; + width: 1%; } .label { @@ -407,6 +541,8 @@ export function generateHTML(summarySection, orgSections, missingMirrorsSection, ${summarySection} + ${recentActivitySection} +
@@ -420,9 +556,9 @@ export function generateHTML(summarySection, orgSections, missingMirrorsSection, const query = e.target.value.toLowerCase().trim(); // Filter repo cards in org sections - document.querySelectorAll('.two-columns .col').forEach(function(col) { - const text = col.textContent.toLowerCase(); - col.style.display = !query || text.includes(query) ? '' : 'none'; + document.querySelectorAll('.repo-card').forEach(function(card) { + const text = card.textContent.toLowerCase(); + card.style.display = !query || text.includes(query) ? '' : 'none'; }); // Filter table rows in sortable tables