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
11 changes: 10 additions & 1 deletion assets/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ button[data-state="success"] { background: var(--color-good); }
.repo-meta strong, .repo-meta span { display: block; }
.repo-meta strong { font: 600 var(--text-lg) var(--font-display); }
.repo-card > a { margin-top: var(--space-lg); font-family: var(--font-mono); font-size: var(--text-sm); }
.milestone-tabs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: var(--rule-thin) solid var(--color-ink); border-bottom: 0; }
.milestone-tab { display: flex; justify-content: space-between; gap: var(--space-md); padding: var(--space-md) var(--space-lg); border: 0; border-right: var(--rule-thin) solid var(--color-ink); background: var(--color-paper); color: var(--color-ink); font: 600 var(--text-sm)/1 var(--font-display); text-align: left; cursor: pointer; }
.milestone-tab:last-child { border-right: 0; }
.milestone-tab:hover { background: var(--color-paper-deep); }
.milestone-tab[aria-selected="true"] { background: var(--color-ink); color: var(--color-paper); }
.milestone-tab-count { font-family: var(--font-mono); color: var(--color-accent); }
.milestone-list { border-top: var(--rule-thin) solid var(--color-ink); }
.milestone-row { display: grid; grid-template-columns: minmax(12rem, 0.8fr) minmax(0, 2fr) auto; gap: var(--space-xl); align-items: center; padding: var(--space-lg) 0; border-bottom: var(--rule-thin) solid var(--color-rule); }
.milestone-row a { font-family: var(--font-display); font-size: var(--text-lg); font-weight: 600; }
Expand Down Expand Up @@ -91,6 +97,9 @@ footer { width: min(100%, 96rem); margin: 0 auto; padding: var(--space-2xl) clam
.section-head { grid-template-columns: minmax(0, 1fr); align-items: start; }
.section-head > a, .section-head > span { justify-self: start; white-space: normal; }
.repo-grid, .gate-columns { grid-template-columns: minmax(0, 1fr); }
.milestone-tabs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.milestone-tab:nth-child(2) { border-right: 0; }
.milestone-tab:nth-child(-n + 2) { border-bottom: var(--rule-thin) solid var(--color-ink); }
.milestone-row { grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-md); }
.milestone-row .progress { grid-column: 1 / -1; grid-row: 2; }
footer { flex-direction: column; }
Expand All @@ -104,4 +113,4 @@ footer { width: min(100%, 96rem); margin: 0 auto; padding: var(--space-2xl) clam
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: 1ms !important; animation-iteration-count: 1 !important; transition-duration: 1ms !important; }
}
}
68 changes: 61 additions & 7 deletions assets/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,65 @@ const escapeHtml = (value = "") => String(value).replace(/[&<>'"]/g, (char) => (
const number = (value) => new Intl.NumberFormat("en-US").format(value || 0);
const dateTime = (value) => value ? new Intl.DateTimeFormat("en-GB", { dateStyle: "medium", timeStyle: "short", timeZone: "Asia/Bangkok" }).format(new Date(value)) : "Not available";

function renderMilestoneTabs(repositories) {
const definitions = [
{ key: "video", label: "Video" },
{ key: "shared", label: "Shared" },
{ key: "document", label: "Document" },
{ key: "manga", label: "Manga" }
];
const repositoryByKey = new Map(repositories.map((repo) => [repo.key, repo]));
const available = definitions.filter(({ key }) => repositoryByKey.has(key));
const requestedKey = location.hash.match(/(?:^#|&)milestones=([a-z]+)/)?.[1];
let activeKey = available.some(({ key }) => key === requestedKey) ? requestedKey : "video";
if (!repositoryByKey.has(activeKey)) activeKey = available[0]?.key;

const tabs = $("#milestone-tabs");
const list = $("#milestone-list");
tabs.innerHTML = available.map(({ key, label }) => {
const count = repositoryByKey.get(key).milestones.length;
const selected = key === activeKey;
return `<button id="milestone-tab-${key}" class="milestone-tab" type="button" role="tab" aria-selected="${selected}" aria-controls="milestone-list" tabindex="${selected ? 0 : -1}" data-repo="${key}"><span>${label}</span><span class="milestone-tab-count">${count}</span></button>`;
}).join("");

const selectRepository = (key, moveFocus = false) => {
const repo = repositoryByKey.get(key);
if (!repo) return;
activeKey = key;
tabs.querySelectorAll("[role=tab]").forEach((tab) => {
const selected = tab.dataset.repo === key;
tab.setAttribute("aria-selected", selected);
tab.tabIndex = selected ? 0 : -1;
});
list.setAttribute("aria-labelledby", `milestone-tab-${key}`);
list.innerHTML = repo.milestones.length ? repo.milestones.map((item) => {
const total = item.openIssues + item.closedIssues;
const percent = total ? Math.round((item.closedIssues / total) * 100) : 0;
return `<div class="milestone-row"><a href="${escapeHtml(item.url)}" target="_blank" rel="noreferrer">${escapeHtml(item.title)}</a><div class="progress" aria-label="${percent}% complete"><span style="width:${percent}%"></span></div><span class="milestone-count">${item.closedIssues}/${total} done</span></div>`;
}).join("") : `<p class="empty-state">No open milestones for ${escapeHtml(repo.displayName)}.</p>`;
history.replaceState(null, "", `${location.pathname}${location.search}#milestones=${key}`);
if (moveFocus) tabs.querySelector(`[data-repo="${key}"]`)?.focus();
};

tabs.addEventListener("click", (event) => {
const tab = event.target.closest("[role=tab]");
if (tab) selectRepository(tab.dataset.repo);
});
tabs.addEventListener("keydown", (event) => {
const keys = available.map(({ key }) => key);
const current = keys.indexOf(activeKey);
let next = current;
if (event.key === "ArrowRight") next = (current + 1) % keys.length;
else if (event.key === "ArrowLeft") next = (current - 1 + keys.length) % keys.length;
else if (event.key === "Home") next = 0;
else if (event.key === "End") next = keys.length - 1;
else return;
event.preventDefault();
selectRepository(keys[next], true);
});
selectRepository(activeKey);
}

function render(data) {
$("#last-sync").textContent = `${dateTime(data.generatedAt)} ICT`;
$("#metric-repos").textContent = number(data.totals.repositories);
Expand Down Expand Up @@ -31,12 +90,7 @@ function render(data) {
const healthy = data.repositories.filter((repo) => repo.latestWorkflow === "success" || repo.latestWorkflow === "--").length;
$("#repo-health").textContent = `${healthy} of ${data.repositories.length} repositories clear`;

const milestones = data.repositories.flatMap((repo) => repo.milestones.map((milestone) => ({ ...milestone, repo: repo.displayName })));
$("#milestone-list").innerHTML = milestones.length ? milestones.slice(0, 16).map((item) => {
const total = item.openIssues + item.closedIssues;
const percent = total ? Math.round((item.closedIssues / total) * 100) : 0;
return `<div class="milestone-row"><a href="${escapeHtml(item.url)}" target="_blank" rel="noreferrer">${escapeHtml(item.repo)} / ${escapeHtml(item.title)}</a><div class="progress" aria-label="${percent}% complete"><span style="width:${percent}%"></span></div><span class="milestone-count">${item.closedIssues}/${total} done</span></div>`;
}).join("") : '<p class="empty-state">No open milestones.</p>';
renderMilestoneTabs(data.repositories);

const issueList = (items, empty) => items.length ? items.slice(0, 12).map((item) => `<a class="issue-item" href="${escapeHtml(item.url)}" target="_blank" rel="noreferrer">${escapeHtml(item.title)}<span>${escapeHtml(item.repository)} / ${escapeHtml(item.targetVersion || "No target")}</span></a>`).join("") : `<p class="empty-state">${empty}</p>`;
$("#gate-list").innerHTML = issueList(data.project.requiredGates, "No required gates.");
Expand All @@ -62,4 +116,4 @@ async function load() {
}
}
$("#refresh-data").addEventListener("click", load);
load();
load();
5 changes: 3 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ <h1 id="hero-title">Four products.<br>One operational truth.</h1>
<div><p>Release authority</p><h2 id="milestones-title">Active milestones</h2></div>
<span>Progress is calculated from linked issues.</span>
</div>
<div id="milestone-list" class="milestone-list"></div>
<div id="milestone-tabs" class="milestone-tabs" role="tablist" aria-label="Repository milestones"></div>
<div id="milestone-list" class="milestone-list" role="tabpanel" tabindex="0"></div>
</section>

<section id="gates" class="gates reveal" aria-labelledby="gates-title">
Expand All @@ -89,4 +90,4 @@ <h1 id="hero-title">Four products.<br>One operational truth.</h1>
</footer>
<noscript>This dashboard requires JavaScript to render GitHub data.</noscript>
</body>
</html>
</html>
Loading