app/tabs/overview.mjs (245 lines of code) (raw):

import { _, localiseNumbers, updateTemplate } from "util"; import * as BugTable from "bugtable"; import * as Bugzilla from "bugzilla"; import * as Global from "global"; import * as Menus from "menus"; import * as Tooltips from "tooltips"; /* global tippy */ const g = { openReady: false, trendsReady: false, me: {}, closedBugFilter: "all", }; function calcMaintEffect(s1, s2, s3, s4, su) { return s1 * 8 + s2 * 5 + s3 * 2 + s4 * 1 + su * 3; } export function initUI() { const $content = _("#overview-content"); BugTable.append({ id: "openDefects", $container: $content, template: "#overview-template", tableContiner: ".open-defects", updateFunc: updateOpenDefects, }); BugTable.append({ id: "trendsDefects", $container: $content, template: "#overview-template", tableContiner: ".trends-defects", updateFunc: updateTrendsDefects, }); const $menuAction = _("#closed-filter-btn").closest(".action"); Menus.initOptionsMenu( $menuAction, _("#closed-filter-menu-template"), () => { return g.closedBugFilter; }, (value, text) => { g.closedBugFilter = value; Tooltips.set($menuAction, value === "all" ? "" : text); _("#trendsDefects .refresh-btn").click(); }, ); } async function updateOpenDefects() { g.openReady = false; // build query urls; note these are count_only, only returning the bug count const queries = {}; for (const severity of ["s1", "s2", "s3", "s4", "--"]) { const name = severity === "--" ? "su" : severity; queries[name] = Bugzilla.queryURL( { resolution: "---", type: "defect", severity: severity, count_only: "1", }, Global.selectedComponents(), "id", ); } await BugTable.updateWrapper( "openDefects", async () => await Promise.all( Object.values(queries).map((url) => Bugzilla.rest(url, false, true)), ), (response) => { // coalesce response into template vars const names = Object.keys(queries); const vars = { sa: 0 }; for (const [i, name] of names.entries()) { vars[name] = response[i].bug_count; vars[`${name}url`] = Bugzilla.queryUrlToBuglistUrl(queries[name]); vars.sa += vars[name]; } vars.saurl = Bugzilla.queryUrlToBuglistUrl( Bugzilla.queryURL( { resolution: "---", type: "defect", }, Global.selectedComponents(), ), ); return vars; }, ); g.openReady = true; await updateBurnDown(); } async function updateTrendsDefects() { g.trendsReady = false; // build queries const queries = {}; for (const weeks of ["1", "4", "12"]) { let query = { type: "defect", f1: "creation_ts", o1: "changedafter", v1: `-${weeks}w`, }; queries[`w${weeks}_opened`] = Bugzilla.queryURL( query, Global.selectedComponents(), "id,severity", ); query = { type: "defect", f1: "cf_last_resolved", o1: "changedafter", v1: `-${weeks}w`, }; if (g.closedBugFilter !== "all") { query.f2 = "creation_ts"; query.o2 = "changedafter"; query.v2 = `-${g.closedBugFilter}`; } queries[`w${weeks}_closed`] = Bugzilla.queryURL( query, Global.selectedComponents(), "id,severity", ); } await BugTable.updateWrapper( "trendsDefects", async () => await Promise.all(Object.values(queries).map((url) => Bugzilla.rest(url))), (response) => { // coalesce response into template vars const names = Object.keys(queries); const vars = {}; for (const [i, name] of names.entries()) { for (const severity of ["sa", "s1", "s2", "s3", "s4", "su"]) { vars[`${name}_${severity}`] = 0; let bugSeverity; switch (severity) { case "sa": { bugSeverity = ""; break; } case "su": { bugSeverity = "--"; break; } default: { bugSeverity = severity; } } vars[`${name}_${severity}url`] = Bugzilla.queryUrlToBuglistUrl( queries[name], { bug_severity: bugSeverity }, ); } for (const bug of response[i].bugs) { const severity = /^S\d$/.test(bug.severity) ? bug.severity.toLowerCase() : "su"; vars[`${name}_${severity}`]++; vars[`${name}_sa`]++; } vars[`${name}_saurl`] = Bugzilla.queryUrlToBuglistUrl(queries[name]); // store totals to be used later for burndown calculation const [week, state] = name.split("_"); if (!(week in g.me)) { g.me[week] = { opened: 0, closed: 0, perc: 0 }; } g.me[week][state] = calcMaintEffect( vars[`${name}_s1`], vars[`${name}_s2`], vars[`${name}_s3`], vars[`${name}_s4`], vars[`${name}_su`], ); } // calc period-level maint-effect for (const week of Object.keys(g.me)) { if (!/^w\d+$/.test(week)) continue; const perc = g.me[week].opened > 0 ? (g.me[week].closed / g.me[week].opened) * 100 : (g.me[week].closed + 1) * 100; g.me[week].perc = perc; vars[`${week}_me`] = `${Math.round(perc)}%`; } return vars; }, ); g.trendsReady = true; await updateBurnDown(); } async function updateBurnDown() { if (!(g.openReady && g.trendsReady)) return; const queries = {}; for (const severity of ["s1", "s2", "s3", "s4", "--"]) { const name = severity === "--" ? "su" : severity; const query = { resolution: "---", type: "defect", severity: severity, count_only: "1", }; if (g.closedBugFilter !== "all") { query.f1 = "creation_ts"; query.o1 = "changedafter"; query.v1 = `-${g.closedBugFilter}`; } queries[name] = Bugzilla.queryURL(query, Global.selectedComponents(), "id"); } const $trendsDefects = _("#trendsDefects"); $trendsDefects.classList.add("loading"); $trendsDefects.classList.remove("error"); let response; try { response = await Promise.all( Object.values(queries).map((url) => Bugzilla.rest(url)), ); $trendsDefects.classList.remove("loading"); } catch (error) { $trendsDefects.classList.remove("loading"); $trendsDefects.classList.add("error"); // eslint-disable-next-line no-console console.error(error); document.body.classList.add("global-error"); return; } const names = Object.keys(queries); let vars = {}; for (const [i, name] of names.entries()) { vars[name] = response[i].bug_count; } g.me.open = calcMaintEffect(vars.s1, vars.s2, vars.s3, vars.s4, vars.su); vars = {}; for (const week of [1, 4, 12]) { const key = `w${week}`; vars[`w${week}_bd`] = g.me[key].opened >= g.me[key].closed ? "∞" : (g.me.open / (g.me[key].closed - g.me[key].opened)) * (week / 52); } localiseNumbers(vars); updateTemplate(_("#trendsDefects"), vars); }