318 lines
9.3 KiB
JavaScript
318 lines
9.3 KiB
JavaScript
(function () {
|
||
var POLL_MS = 30000;
|
||
var MONTHS = [
|
||
"jan",
|
||
"feb",
|
||
"mar",
|
||
"apr",
|
||
"may",
|
||
"jun",
|
||
"jul",
|
||
"aug",
|
||
"sep",
|
||
"oct",
|
||
"nov",
|
||
"dec",
|
||
];
|
||
|
||
function cookieEdition() {
|
||
var m = document.cookie.match(/(?:^|; )tdpd_edition=([^;]*)/);
|
||
if (!m) return "flask";
|
||
try {
|
||
return decodeURIComponent(m[1]) || "flask";
|
||
} catch (e) {
|
||
return "flask";
|
||
}
|
||
}
|
||
|
||
function esc(s) {
|
||
return String(s)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
/** ISO → "sep30 16:12" (3-letter English month + DD + HH:MM). */
|
||
function shortTime(iso) {
|
||
if (!iso) return "";
|
||
var m = String(iso).match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2})/);
|
||
if (m) {
|
||
var mon = MONTHS[Number(m[2]) - 1] || m[2];
|
||
return mon + m[3] + " " + m[4];
|
||
}
|
||
return String(iso).slice(0, 16).replace("T", " ");
|
||
}
|
||
|
||
function fmtBuildSec(sec) {
|
||
if (sec == null || sec === "") return "";
|
||
var n = Number(sec);
|
||
if (!isFinite(n)) return "";
|
||
if (n < 10) return n.toFixed(1) + "s";
|
||
return Math.round(n) + "s";
|
||
}
|
||
|
||
function builtText(row) {
|
||
var sec = fmtBuildSec(row.build_seconds);
|
||
var at = shortTime(row.built_at || "");
|
||
if (sec && at) return sec + " " + at;
|
||
return sec || at;
|
||
}
|
||
|
||
function plusHtml(row) {
|
||
if (row.vs_parent_plus != null) {
|
||
var p = Number(row.vs_parent_plus);
|
||
if (!isFinite(p)) return "";
|
||
return '<span class="diff-plus">+' + esc(String(p)) + "</span>";
|
||
}
|
||
if (row.loc != null) {
|
||
var n = Number(row.loc);
|
||
if (!isFinite(n)) return "";
|
||
return '<span class="diff-loc">' + esc(String(n)) + "</span>";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function minusHtml(row) {
|
||
if (row.vs_parent_minus != null) {
|
||
var m = Number(row.vs_parent_minus);
|
||
if (!isFinite(m)) return "";
|
||
return '<span class="diff-minus">−' + esc(String(m)) + "</span>";
|
||
}
|
||
if (row.loc != null) {
|
||
return '<span class="diff-loc-unit">LOC</span>';
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function setText(el, text) {
|
||
if (!el) return;
|
||
var t = text || "";
|
||
if (el.textContent !== t) el.textContent = t;
|
||
}
|
||
|
||
function rowTitle(row) {
|
||
var tip = row.tip || "";
|
||
var tipTime = row.tip_time || "";
|
||
var vs = "";
|
||
if (row.vs_parent_plus != null || row.vs_parent_minus != null) {
|
||
vs =
|
||
" vs parent +" +
|
||
String(row.vs_parent_plus == null ? 0 : row.vs_parent_plus) +
|
||
"/−" +
|
||
String(row.vs_parent_minus == null ? 0 : row.vs_parent_minus);
|
||
} else if (row.loc != null) {
|
||
vs = " loc=" + String(row.loc);
|
||
}
|
||
var built = builtText(row);
|
||
return (
|
||
"Switch build to " +
|
||
(row.branch || "") +
|
||
(tip ? " @ " + tip : "") +
|
||
(tipTime ? " tip=" + shortTime(tipTime) : "") +
|
||
(built ? " build=" + built : "") +
|
||
vs
|
||
);
|
||
}
|
||
|
||
function structureKey(list) {
|
||
var parts = [];
|
||
for (var i = 0; i < list.length; i++) {
|
||
var row = list[i];
|
||
parts.push(
|
||
(row.edition || "") +
|
||
"\t" +
|
||
(row.branch || "") +
|
||
"\t" +
|
||
(row.prefix || "") +
|
||
"\t" +
|
||
(row.parent || "")
|
||
);
|
||
}
|
||
return parts.join("\n");
|
||
}
|
||
|
||
function pinBar(timingMsg) {
|
||
var bar = document.getElementById("cascade-bar");
|
||
if (!bar) return;
|
||
if (timingMsg) {
|
||
bar.setAttribute("data-timing", timingMsg);
|
||
bar.setAttribute("title", timingMsg);
|
||
}
|
||
var h = bar.offsetHeight || 0;
|
||
document.documentElement.style.setProperty("--cascade-pad", h + "px");
|
||
}
|
||
|
||
function markActive(root, edition) {
|
||
var links = root.querySelectorAll("a[data-edition]");
|
||
for (var i = 0; i < links.length; i++) {
|
||
var a = links[i];
|
||
var ed = a.getAttribute("data-edition");
|
||
var on =
|
||
ed === edition ||
|
||
(edition === "flask" && a.getAttribute("data-default") === "1");
|
||
if (on) {
|
||
a.classList.add("is-active");
|
||
a.setAttribute("aria-current", "page");
|
||
} else {
|
||
a.classList.remove("is-active");
|
||
a.removeAttribute("aria-current");
|
||
}
|
||
}
|
||
}
|
||
|
||
function timingText(timing, listLen) {
|
||
if (!timing || timing.wall_seconds == null) return "";
|
||
return (
|
||
"last full pass ~" +
|
||
String(timing.wall_seconds) +
|
||
"s · built " +
|
||
String(timing.built || 0) +
|
||
" / skipped " +
|
||
String(timing.skipped || 0) +
|
||
" / " +
|
||
String(timing.branches || listLen)
|
||
);
|
||
}
|
||
|
||
function updateValues(mount, data) {
|
||
var list = (data && data.branches) || [];
|
||
var defaultBranch = (data && data.default_branch) || "feat/website-views";
|
||
var timing = (data && data.rebuild_timing) || null;
|
||
var rows = mount.querySelectorAll("a.cascade-tree-row[data-edition]");
|
||
if (rows.length !== list.length) return false;
|
||
for (var i = 0; i < list.length; i++) {
|
||
var row = list[i];
|
||
var a = rows[i];
|
||
if (a.getAttribute("data-edition") !== (row.edition || "")) return false;
|
||
if (a.getAttribute("data-branch") !== (row.branch || "")) return false;
|
||
var isDefault = (row.branch || "") === defaultBranch;
|
||
if (isDefault) a.setAttribute("data-default", "1");
|
||
else a.removeAttribute("data-default");
|
||
setText(a.querySelector(".cascade-when"), shortTime(row.tip_time || ""));
|
||
setText(a.querySelector(".cascade-built"), builtText(row));
|
||
var plusEl = a.querySelector(".cascade-plus");
|
||
if (plusEl) {
|
||
var ph = plusHtml(row);
|
||
if (plusEl.innerHTML !== ph) plusEl.innerHTML = ph;
|
||
}
|
||
var minusEl = a.querySelector(".cascade-minus");
|
||
if (minusEl) {
|
||
var mh = minusHtml(row);
|
||
if (minusEl.innerHTML !== mh) minusEl.innerHTML = mh;
|
||
}
|
||
a.setAttribute("title", rowTitle(row));
|
||
}
|
||
var timingEl = mount.querySelector(".cascade-timing");
|
||
var t = timingText(timing, list.length);
|
||
if (t) {
|
||
if (!timingEl) {
|
||
timingEl = document.createElement("div");
|
||
timingEl.className = "cascade-timing";
|
||
mount.appendChild(timingEl);
|
||
}
|
||
if (timingEl.textContent !== t) timingEl.textContent = t;
|
||
} else if (timingEl) {
|
||
timingEl.parentNode.removeChild(timingEl);
|
||
}
|
||
markActive(mount, cookieEdition());
|
||
pinBar(t);
|
||
return true;
|
||
}
|
||
|
||
function renderTree(data, soft) {
|
||
var mount = document.getElementById("cascade-tree");
|
||
if (!mount) return;
|
||
var list = (data && data.branches) || [];
|
||
if (!list.length) {
|
||
mount.textContent = "No branches in branch-status.json";
|
||
pinBar();
|
||
return;
|
||
}
|
||
if (soft && mount.getAttribute("data-structure") === structureKey(list)) {
|
||
if (updateValues(mount, data)) return;
|
||
}
|
||
var defaultBranch = (data && data.default_branch) || "feat/website-views";
|
||
var timing = (data && data.rebuild_timing) || null;
|
||
var html = [];
|
||
html.push('<div class="cascade-tree-lines" role="group">');
|
||
for (var i = 0; i < list.length; i++) {
|
||
var row = list[i];
|
||
var edition = row.edition || "";
|
||
var branch = row.branch || "";
|
||
var prefix = row.prefix || "";
|
||
var parent = row.parent || "";
|
||
var isDefault = branch === defaultBranch;
|
||
html.push(
|
||
'<a class="cascade-tree-row" role="treeitem" href="/_cascade/use/' +
|
||
esc(edition) +
|
||
'" data-edition="' +
|
||
esc(edition) +
|
||
'" data-branch="' +
|
||
esc(branch) +
|
||
'"' +
|
||
(parent ? ' data-parent="' + esc(parent) + '"' : "") +
|
||
(isDefault ? ' data-default="1"' : "") +
|
||
' title="' +
|
||
esc(rowTitle(row)) +
|
||
'">' +
|
||
'<span class="cascade-name">' +
|
||
esc(prefix) +
|
||
"<code>" +
|
||
esc(branch) +
|
||
"</code></span>" +
|
||
'<span class="cascade-when">' +
|
||
esc(shortTime(row.tip_time || "")) +
|
||
"</span>" +
|
||
'<span class="cascade-built">' +
|
||
esc(builtText(row)) +
|
||
"</span>" +
|
||
'<span class="cascade-plus">' +
|
||
plusHtml(row) +
|
||
"</span>" +
|
||
'<span class="cascade-minus">' +
|
||
minusHtml(row) +
|
||
"</span>" +
|
||
"</a>"
|
||
);
|
||
}
|
||
html.push("</div>");
|
||
var t = timingText(timing, list.length);
|
||
if (t) {
|
||
html.push('<div class="cascade-timing">' + esc(t) + "</div>");
|
||
}
|
||
mount.innerHTML = html.join("");
|
||
mount.setAttribute("data-structure", structureKey(list));
|
||
markActive(mount, cookieEdition());
|
||
pinBar(t);
|
||
}
|
||
|
||
function loadStatus(soft) {
|
||
var mount = document.getElementById("cascade-tree");
|
||
return fetch("/branch-status.json", { cache: "no-store" })
|
||
.then(function (res) {
|
||
if (!res.ok) throw new Error("status");
|
||
return res.json();
|
||
})
|
||
.then(function (data) {
|
||
renderTree(data, !!soft);
|
||
})
|
||
.catch(function () {
|
||
if (!soft && mount && !mount.querySelector("a[data-edition]")) {
|
||
mount.innerHTML =
|
||
'<a class="cascade-tree-row" href="/_cascade/use/flask" data-edition="flask" data-branch="flask"><span class="cascade-name"><code>flask</code></span></a>' +
|
||
'<span class="tip"> (branch-status unavailable)</span>';
|
||
markActive(mount, cookieEdition());
|
||
pinBar();
|
||
}
|
||
});
|
||
}
|
||
|
||
loadStatus(false);
|
||
setInterval(function () {
|
||
loadStatus(true);
|
||
}, POLL_MS);
|
||
window.addEventListener("resize", function () {
|
||
pinBar();
|
||
});
|
||
})();
|