js/needinfo.js (419 lines of code) (raw):
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var NeedInfoConfig = null;
var DEV_DISABLE = false;
var LastErrorText = '';
var CurrentTeam = null;
var PageStats;
/*
Bugs
* multiple puts with the same comment results in errors on every request except
the first. Hits some sort of Bugzilla anti-spam feature?
General Ideas
* performance - need to decrease the number of queries
* Add an account icon up top for employees? Can we do this for key users as well?
* Bugzilla aliases for NI email entries in details
* set Priority and Severity bulk action?
* redirect to triage owner based on google cal injestion?
* Bug assingee changes by nagbots, can we expose who got dropped?
- https://bugzilla.mozilla.org/show_bug.cgi?id=969395
* Dealing with double needinfos
- https://bugzilla.mozilla.org/show_bug.cgi?id=1667635#c31
Details pane
* how to expose more information without adding columns?
* - work on the filtering option
* - bug creation date
* - component
* - sliding drop down with bug detail?
* resolve incomplete feature?
*/
// 'main'
$(document).ready(function () {
loadConfig();
});
// Load the Bugzilla configuration json file and populate our NeedInfoConfig data
// structure with config information. Then process url parameter input and choose
// the appropriate data to display.
function loadConfig() {
let jsonUrl = 'js/config.json';
$.getJSON(jsonUrl, function (configdata) {
NeedInfoConfig = configdata.bugzillaconfig;
updateDomains(NeedInfoConfig);
let team = getTeam();
CurrentTeam = team;
if (team == undefined) {
window.location.href = window.location.href + "?team=empty"
return;
}
let sel = document.getElementById('team-select');
sel.value = team;
let userid = getUserId();
if (userid != undefined && team == 'empty') {
// individual account request
loadUserSummary(userid);
// get empty-option
document.getElementById('empty-option').text = userid;
return;
}
// Load the team summary
loadTeamConfig();
}).fail(function (jqXHR, textStatus, errorThrown) {
console.log("getJSON call failed for some reason.", jsonUrl, errorThrown)
});
}
// Load a summary for a specific bugzilla email account
function loadUserSummary(email) {
CurrentTeam = 'empty';
let sel = document.getElementById('team-select');
sel.value = CurrentTeam;
// populate additional cookie data in NeedInfoConfig
loadSettingsInternal();
document.getElementById('oldest-search-date').value = NeedInfoConfig.lastdate == null ?
getTodaysDateMinusOneYear() : NeedInfoConfig.lastdate;
LastErrorText = '';
$("#errors").empty();
NeedInfoConfig.developers = {};
NeedInfoConfig.developers[email] = email;
loadPage();
}
function event_loadUserSummary(e, developer) {
if (e) {
e.preventDefault();
}
let userid = NeedInfoConfig.developers[developer];
openDetailsForAccount(userid);
}
function openDetailsForAccount(email) {
let url = replaceUrlParam(window.location.href, 'userid', email);
url = replaceUrlParam(url, 'team', 'empty')
window.location.href = url;
}
// Load a summary for a configured team based on
// the team's json config file.
function loadTeamConfig() {
// populate additional cookie data in NeedInfoConfig
loadSettingsInternal();
document.getElementById('oldest-search-date').value = NeedInfoConfig.lastdate == null ?
getTodaysDateMinusOneYear() : NeedInfoConfig.lastdate;
NeedInfoConfig.developers = {};
LastErrorText = '';
$("#errors").empty();
prepPage();
var team = getTeam();
if (team == 'empty') {
// don't load anything.
return;
}
// load a specific team config file.
let jsonUrl = 'js/' + team + '.json';
$.getJSON(jsonUrl, function (teamConfig) {
NeedInfoConfig.developers = teamConfig.developers;
loadPage();
}).fail(function (jqXHR, textStatus, errorThrown) {
console.log("getJSON call failed for some reason.", jsonUrl, errorThrown)
});
}
function prepPage() {
resetPageStats();
$("#report").empty();
var content =
"<div class='report-title'>Engineer</div>" +
"<div class='report-odr'>Open Dev Related</div>" +
"<div class='report-otr' title='Bugs with any needinfos for you that track the current nightly, beta, or release versions of Firefox.'>Open Tracked</div>" +
"<div class='report-cdr'>Closed Dev Related</div>" +
"<div class='report-onb'>Open Nagbot</div>" +
"<div class='report-cnb'>Closed Nagbot</div>";
// This is an elemet id idx and must match up when we
// populate each row in displayCountFor.
let devIndex = 0;
for (let developer in NeedInfoConfig.developers) {
devIndex++;
content +=
"<a href='' onclick=\"event_loadUserSummary(event, '" + developer + "')\"><div class='report-title'>" + developer + "</div></a>" +
"<div class='report-odr' id='data_odr_" + devIndex + "'>?</div>" +
"<div class='report-otr' id='data_otr_" + devIndex + "'>?</div>" +
"<div class='report-cdr' id='data_cdr_" + devIndex + "'>?</div>" +
"<div class='report-onb' id='data_onb_" + devIndex + "'>?</div>" +
"<div class='report-cnb' id='data_cnb_" + devIndex + "'>?</div>";
}
if (content.length) {
$("#report").append(content);
}
if (devIndex > 1) {
content =
"<div class='report-title'>TOTALS</div>" +
"<div class='report-odr' id='odr-total'>"+ PageStats.devOpen + "</div>" +
"<div class='report-otr' id='otr-total'>"+ PageStats.tracked + "</div>" +
"<div class='report-cdr' id='cdr-total'>"+ PageStats.devClosed + "</div>" +
"<div class='report-onb' id='onb-total'>"+ PageStats.nagOpen + "</div>" +
"<div class='report-cnb' id='cnb-total'>"+ PageStats.nagClosed + "</div>";
$("#report").append(content);
}
checkConfig();
}
function resetPageStats() {
PageStats = {
devOpen: 0,
devClosed: 0,
tracked: 0,
nagOpen: 0,
nagClosed: 0
};
}
function populatePageStats() {
if (document.getElementById('odr-total')) {
document.getElementById('odr-total').textContent = PageStats.devOpen;
document.getElementById('otr-total').textContent = PageStats.tracked;
document.getElementById('cdr-total').textContent = PageStats.devClosed;
document.getElementById('onb-total').textContent = PageStats.nagOpen;
document.getElementById('cnb-total').textContent = PageStats.nagClosed;
}
}
function lastDateChanged() {
console.log('lastDateChanged', document.getElementById('oldest-search-date').value);
saveLastDate();
}
function getBugzillaMaxDateQuery() {
let date = null;
if (NeedInfoConfig.lastdate) {
date = NeedInfoConfig.lastdate;
} else {
date = document.getElementById('oldest-search-date').value;
}
//console.log('query last date', date); // '2024-05-08' | bugzilla: '2014-09-29T14:25:35Z'
if (date && date.length) {
// chfieldfrom=2023-05-23&chfield=[Bug creation]
return '&chfield=[Bug creation]' + '&chfieldfrom=' + date;
}
return '';
}
function getMaxDateParameter() {
let date = document.getElementById('oldest-search-date').value;
// console.log('date', date); // '2024-05-08' | bugzilla: '2014-09-29T14:25:35Z'
if (date.length) {
// chfieldfrom=2023-05-23&chfield=[Bug creation]
return '&creation_time=' + date;
}
return '';
}
function loadPage() {
prepPage();
if (DEV_DISABLE)
return;
let elementIndex = 0;
for (let developer in NeedInfoConfig.developers) {
elementIndex++;
// This should be the id of the dev we are querying for, not the
// current user. We use this to filter selfnis when we query for
// each developer's list.
let id = encodeURIComponent(NeedInfoConfig.developers[developer]);
// exclude_fields=_all
// v1={id}
// o1=equals
// f1=requestees.login_name
// f2=flagtypes.name
// o2=equals
// v2=needinfo?
/////////////////////////////////////////////////////////
// Open Developer Related
/////////////////////////////////////////////////////////
let url = NeedInfoConfig.bugzilla_search_url;
if (NeedInfoConfig.api_key.length) {
url += "api_key=" + NeedInfoConfig.api_key + "&";
}
url += NeedInfoConfig.fields_query.replace("{id}", id);
// if requested, max lifetime date
url += getBugzillaMaxDateQuery();
// filter certain needinfo setters
url += "&f3=setters.login_name";
url += "&o3=nowordssubstr";
url += "&v3=release-mgmt-account-bot%40mozilla.tld";
// Ignore needinfos set by the account we're querying for.
if (NeedInfoConfig.ignoremyni) {
url += "," + id;
}
// f4=bug_status
// o4=nowordssubstr / anywordssubstr
// v4=RESOLVED%2CVERIFIED%2CCLOSED
url += "&f4=bug_status";
url += "&o4=nowordssubstr";
url += "&v4=RESOLVED%2CVERIFIED%2CCLOSED";
retrieveInfoFor(url, id, elementIndex, developer, 'odr');
/////////////////////////////////////////////////////////
// Open and Tracked
/////////////////////////////////////////////////////////
// Query uses the generic tag names that bugzilla maps
// to the current version tags.
url = NeedInfoConfig.bugzilla_search_url;
if (NeedInfoConfig.api_key.length) {
url += "api_key=" + NeedInfoConfig.api_key + "&";
}
url += NeedInfoConfig.fields_query.replace("{id}", id);
// if requested, max lifetime date
url += getBugzillaMaxDateQuery();
// f4=bug_status
// o4=nowordssubstr / anywordssubstr
// v4=RESOLVED%2CVERIFIED%2CCLOSED
url += "&f3=bug_status";
url += "&o3=nowordssubstr";
url += "&v3=RESOLVED%2CVERIFIED%2CCLOSED";
// f5=cf_tracking_firefox_beta
// o5=anywords
// v5=%2B
url += '&f4=OP';
url += '&j4=OR';
url += "&f5=cf_tracking_firefox_nightly";
url += "&o5=anywords";
url += "&v5=%2B";
url += "&f6=cf_tracking_firefox_beta";
url += "&o6=anywords";
url += "&v6=%2B";
url += "&f7=cf_tracking_firefox_release";
url += "&o7=anywords";
url += "&v7=%2B";
url += '&f8=CP'
retrieveInfoFor(url, id, elementIndex, developer, 'otr');
/////////////////////////////////////////////////////////
// Closed Developer Related
/////////////////////////////////////////////////////////
url = NeedInfoConfig.bugzilla_search_url;
if (NeedInfoConfig.api_key.length) {
url += "api_key=" + NeedInfoConfig.api_key + "&";
}
url += NeedInfoConfig.fields_query.replace("{id}", id);
// if requested, max lifetime date
url += getBugzillaMaxDateQuery();
url += "&f3=setters.login_name";
url += "&o3=notequals";
url += "&v3=release-mgmt-account-bot%40mozilla.tld";
url += "&f4=bug_status";
url += "&o4=anywordssubstr";
url += "&v4=RESOLVED%2CVERIFIED%2CCLOSED";
// Ignore needinfos set by the account we're querying for.
if (NeedInfoConfig.ignoremyni) {
url += "&f5=setters.login_name";
url += "&o5=notequals";
url += "&v5=" + id;
}
retrieveInfoFor(url, id, elementIndex, developer, 'cdr');
/////////////////////////////////////////////////////////
// Open Nagbot
/////////////////////////////////////////////////////////
url = NeedInfoConfig.bugzilla_search_url;
if (NeedInfoConfig.api_key.length) {
url += "api_key=" + NeedInfoConfig.api_key + "&";
}
url += NeedInfoConfig.fields_query.replace("{id}", id);
// if requested, max lifetime date
url += getBugzillaMaxDateQuery();
url += "&f3=setters.login_name";
url += "&o3=equals";
url += "&v3=release-mgmt-account-bot%40mozilla.tld";
url += "&f4=bug_status";
url += "&o4=nowordssubstr";
url += "&v4=RESOLVED%2CVERIFIED%2CCLOSED";
retrieveInfoFor(url, id, elementIndex, developer, 'onb');
/////////////////////////////////////////////////////////
// Closed Nagbot
/////////////////////////////////////////////////////////
url = NeedInfoConfig.bugzilla_search_url;
if (NeedInfoConfig.api_key.length) {
url += "api_key=" + NeedInfoConfig.api_key + "&";
}
url += NeedInfoConfig.fields_query.replace("{id}", id);
// if requested, max lifetime date
url += getBugzillaMaxDateQuery();
url += "&f3=setters.login_name";
url += "&o3=equals";
url += "&v3=release-mgmt-account-bot%40mozilla.tld";
url += "&f4=bug_status";
url += "&o4=anywordssubstr";
url += "&v4=RESOLVED%2CVERIFIED%2CCLOSED";
retrieveInfoFor(url, id, elementIndex, developer, 'cnb');
}
}
function refreshList(e) {
if (e) {
e.preventDefault();
}
loadConfig();
}
function teamSelectionChanged(el) {
var team = el.options[el.selectedIndex].value;
if (team == 'specific-account') {
queryAccount();
return;
}
window.location.href = replaceUrlParam(window.location.href, 'team', team);
}
function errorMsg(text) {
if (LastErrorText == text)
return;
$("#errors").append(text);
LastErrorText = text;
}
// this function's sole reason for existing is to provide
// a capture context for the AJAX values...
function retrieveInfoFor(url, id, elementIndex, developer, userQuery) {
$.ajax({
url: url,
success: function (data) {
displayCountFor(id, elementIndex, developer, url, userQuery, data);
}
})
.error(function(jqXHR, textStatus, errorThrown) {
displayErrorFor(id, elementIndex, developer, url, userQuery);
console.log("status:", textStatus);
console.log("error thrown:", errorThrown);
console.log("response text:", jqXHR.responseText);
try {
let info = JSON.parse(jqXHR.responseText);
let text = info.message ? info.message : errorThrown;
errorMsg(text);
return;
} catch(e) {
}
errorMsg(errorThrown);
});
}
function displayErrorFor(id, elementIndex, developer, url, type) {
}
function displayCountFor(id, elementIndex, developer, url, type, data) {
let ni_count = 0;
if (data && data.bugs) {
ni_count = data.bugs.length;
}
let tabTarget = NeedInfoConfig.targetnew ? "buglists" : "_blank";
let bug_link = "" + ni_count;
if (ni_count != 0) {
let dash_link = "details.html?" + "&userquery=" + type + "&userid=" + id + getMaxDateParameter();
let bug_list = restToQueryUrl(url);
bug_link = "<div class='bug-link-container'><a class='bug-link' title='Needinfo Details' href='" + dash_link + "' target='nilist'>" + ni_count + "</a>";
bug_link += "<a class='bug-icon' title='Bugzilla Bug List' href='" + bug_list + "' target='" + tabTarget + "'><img src='images/favicon.ico' /></a></div>";
}
if (!ni_count) {
bug_link = " ";
}
switch(type) {
case 'odr':
PageStats.devOpen += ni_count;
break;
case 'otr':
PageStats.tracked += ni_count;
break;
case 'cdr':
PageStats.devClosed += ni_count;
break;
case 'onb':
PageStats.nagOpen += ni_count;
break;
case 'cnb':
PageStats.nagClosed += ni_count;
break;
}
populatePageStats();
let link = "<div class='report-" + type + "' id='data_" + elementIndex + "'>" + bug_link + "</div>";
$("#data_" + type + "_" + elementIndex).replaceWith(link);
}
function settingsUpdated() {
checkConfig();
refreshList(null);
}
function checkConfig() {
if (!NeedInfoConfig.api_key || NeedInfoConfig.api_key.length == 0) {
document.getElementById('alert-icon').style.visibility = 'visible';
} else {
document.getElementById('alert-icon').style.visibility = 'hidden';
}
}
function getRedirectToAccount() {
if (!document.getElementById('autofill-user-search').disabled &&
document.getElementById('autofill-user-search').value) {
return document.getElementById('autofill-user-search').value;
}
let to = document.getElementById("prompt-redirect-to-confirm-to").value;
if (!to.length) {
to = null;
}
return to;
}
function queryAccount() {
let dlg = document.getElementById("prompt-query-account");
dlg.returnValue = "cancel";
dlg.addEventListener('close', (event) => {
if (dlg.returnValue == 'confirm') {
let to = getRedirectToAccount();
if (to != null) {
// Open a details page for a specific account
openDetailsForAccount(to);
}
} else {
}
}, { once: true });
dlg.show();
}
function submitUserSearch(value) {
let url = NeedInfoConfig.bugzilla_user_url;
url = url.replace('{value}', value);
if (NeedInfoConfig.api_key.length) {
url += "&api_key=" + NeedInfoConfig.api_key;
}
$('#autofill-user-search').empty();
$.ajax({
url: url,
success: function (data) {
// data.users.name and real_name
data.users.forEach(function (val) {
let name = "" + val.real_name;
let email = "" + val.name;
if (name.length == 0) {
name = ' ';
}
name += ' (' + email + ')';
$('#autofill-user-search').append(new Option(name, email));
});
document.getElementById('autofill-user-search').disabled = false;
}
})
.error(function(jqXHR, textStatus, errorThrown) {
console.log("status:", textStatus);
console.log("error thrown:", errorThrown);
console.log("response text:", jqXHR.responseText);
try {
let info = JSON.parse(jqXHR.responseText);
let text = info.message ? info.message : errorThrown;
errorMsg(text);
return;
} catch(e) {
}
errorMsg(errorThrown);
});
}
function searchForNick(element) {
$('#autofill-user-search').empty();
document.getElementById('autofill-user-search').disabled = true;
let value = element.value;
if (!value) {
return;
}
if (value.element < 3) {
return;
}
console.log('searching for', value);
submitUserSearch(value);
}
var SearchTimeoutId = -1;
function onInputForBugzillaUser(element) {
clearTimeout(SearchTimeoutId);
SearchTimeoutId = setTimeout(function () {
searchForNick(element);
}, 750);
}