blob: dc83c3b3ac07ae460e9f5fa9a54e739672ebe32e [file] [log] [blame]
"use strict";
/* This defines the gold namespace which contains all JS code relevant to
gold. Functions that are generic should move to common.js
TODO(stephana): Move everythin the requires to be in sync with the
backend to this file.
*/
var gold = gold || {};
(function(){
// Constants for status values.
gold.POSITIVE = 'positive',
gold.NEGATIVE = 'negative';
gold.UNTRIAGED = 'untriage';
// Reference diffs.
gold.REF_NEG = 'neg';
gold.REF_POS = 'pos';
gold.REF_TRACE = 'trace';
// Metric values.
gold.METRIC_COMBINED = 'combined';
gold.METRIC_PERCENT = 'percent';
gold.METRIC_PIXEL = 'pixel';
gold.allMetrics = [
gold.METRIC_COMBINED,
gold.METRIC_PERCENT,
gold.METRIC_PIXEL,
];
// Operators to apply to images grouped by test.
gold.GROUP_TEST_MAX_COUNT = 'count' // Most often occuring digest.
gold.groupTestOps = [
gold.GROUP_TEST_MAX_COUNT,
];
// ISSUE_TRACKER_URL is the url of the monorail issue tracker.
var ISSUE_TRACKER_URL = 'https://bugs.chromium.org/p/skia/issues/';
// Costants for sort order.
gold.SORT_ASC = 'asc';
gold.SORT_DESC = 'desc';
gold.sortOptions = [
gold.SORT_ASC,
gold.SORT_DESC
];
// Default values for the search controls.
gold.defaultSearchState = {
// The metric to use.
metric: gold.METRIC_COMBINED,
// Sort order.
sort: gold.SORT_DESC,
// Configs that need to match during comparisons.
match: ['name'],
// Note: query is a URL encoded query over the test parameters
// The fields of query are not fixed but change over time. This requires
// to encode/decode a query in a separate step when encoding/decoding
// this entire object.
query:'',
rquery: '',
head: true,
include: false,
pos: false,
neg: false,
unt: true,
blame: '',
limit: 50,
offset: 0,
issue: '',
patchsets: '',
// Filter options.
// Begin and end commits. Must be valid commits.
fbegin: '',
fend: '',
// Select max RGBA difference.
frgbamin: 0,
// Select max RGBA difference.
frgbamax: 255,
// Select max difference.
fdiffmax: -1,
// Group by test and select a specific digest.
fgrouptest: '',
// Only include images that have a reference.
fref: false,
// master indicates whether to include digests that are also in master
// when querying tryjob results.
master: false,
};
// Default values for the search query of the by-blame-page.
gold.defaultByBlameState = {
query: '',
};
// Default values for pagination objects.
gold.defaultPagination = {
size: 50,
offset: 0,
total: 0
};
// Default values for pagination URL state.
gold.defaultPaginationState = {
size: gold.defaultPagination.size,
offset: gold.defaultPagination.offset
};
// Table that maps reference point ids to readable titles.
gold.diffTitles = {}
gold.diffTitles[gold.REF_TRACE] = 'Trace previously';
gold.diffTitles[gold.REF_POS] = 'Closest Positive';
gold.diffTitles[gold.REF_NEG] = 'Closest Negative';
// Return a title for the given reference point id.
gold.getDiffTitle = function(diffType) {
return gold.diffTitles[diffType] || diffType;
};
// Return the URL for the given digest.
gold.imgHref = function(digest) {
if (!digest) {
return '';
}
return '/img/images/' + digest + '.png'
};
// Return the URL for the diff image between the two given digests.
gold.diffImgHref = function(d1, d2) {
if (!d1 || !d2) {
return '';
}
return '/img/diffs/' + ((d1 < d2) ? (d1 + '-' + d2) : (d2 + '-' + d1)) + '.png'
};
// Returns the query string to pass to the diff page or to the diff endpoint.
// Input is the name of the test and the two digests to compare.
gold.diffQuery = function(test, left, right, issue) {
const u = '?test=' + test + '&left=' + left + '&right=' + right;
if (issue) {
return u + '&issue=' +issue;
}
return u;
};
// Returns the query string to use for the detail page or the call to the
// diff endpoint.
gold.detailQuery = function(test, digest, issue) {
const u = '?test=' + test + '&digest=' + digest;
if (issue) {
return u + '&issue=' +issue;
}
return u;
};
// stateFromQuery returns a state object based on the query portion of the URL.
gold.stateFromQuery = function(defaultState) {
var delta = sk.query.toObject(window.location.search.slice(1), defaultState);
return sk.object.applyDelta(delta, defaultState);
};
// filterEmpty returns a copy of the object without fields where the value
// is an empty string.
gold.filterEmpty = function(obj) {
var cpObj = {};
for(var k in obj) {
if (obj.hasOwnProperty(k) && (obj[k] !== '')) {
cpObj[k] = obj[k];
}
}
return cpObj;
};
// queryFromState returns a query string from the the given state object.
gold.queryFromState = function(srcState) {
var ret = sk.query.fromObject(gold.filterEmpty(srcState));
if (ret === '') {
return '';
}
return '?' + ret;
};
// updateParamsConditionally updates the given paramset 'params' with the
// paramset 'updateParamSet' if the item is not present in the first.
// If the 'force' flag it true it will always do the update.
gold.updateParamsConditionally = function(params, updateParamSet, force) {
for(var k in updateParamSet) {
if (updateParamSet.hasOwnProperty(k) && (force || !params.hasOwnProperty(k))) {
params[k] = updateParamSet[k];
}
}
return sk.query.fromParamSet(params);
}
// loadWithActivity sends a GET request to the given url and uses the provide
// acitivity element as an indicator. If the call succeeds it applies the
// parsed result to 'target'.
// If 'target' is a string it will call the 'set' function of the Polymer
// element 'ele' with 'target', if 'target' is a function it will call it.
gold.loadWithActivity = function(ele, url, activity, target) {
activity.startSpinner('Loading...');
sk.get(url).then(JSON.parse).then(function (json) {
activity.stopSpinner();
if (typeof(target) === 'function') {
target(json);
} else {
ele.set(target, json);
}
}).catch(function(e) {
activity.stopSpinner();
sk.errorMessage(e);
});
},
gold.issueURL = function(issueID) {
return ISSUE_TRACKER_URL + '/detail?id=' + issueID;
};
// TriageQuery returns an object that can be sent as a query to the
// backend to triage digests. The arguments can either be a 4-tuple:
// makeTriageQuery(testName, digests, status, issue)
// or a pair:
// makeTriageQuery(arr, issue)
// where 'arr' is an array of triples: <testName, digests, status>.
// Note: 'digests' can either be a single string or an array.
// 'issue' is the id of the code review issue for which we want to triage.
// It has to be a positive integer (> 0) to be considered.
gold.TriageQuery = function(triageList, issue) {
if (arguments.length > 2) {
triageList = [[arguments[0], arguments[1], arguments[2]]];
issue = arguments[3];
}
var ret = {};
triageList.forEach(function(t) {
var test=t[0], digests=t[1], status=t[2];
if (!Array.isArray(digests)) {
digests = [digests];
}
var found = ret[test];
if (!found) {
ret[test] = {};
found = ret[test];
}
for(var i=0; i < digests.length; i++) {
found[digests[i]] = status;
}
});
this.testDigestStatus = ret;
this.setIssue(issue);
};
// setIssue is a setter for the issue of a TriageQuery
// if "" or "0", will be assigned to the master branch.
gold.TriageQuery.prototype.setIssue = function(issue) {
this.issue = issue;
};
// flattenTriageQuery is the inverse operation of makeTriageQuery.
// It returns an array of triples where each triple contains:
// [testName, digests, status]
// testName and status are strings and digests is an array of strings.
gold.flattenTriageQuery = function(q) {
var ret = [];
q = q.testDigestStatus
for(var k in q) {
if (q.hasOwnProperty(k)) {
var statusMap = {};
// iterat over the digests and group by status.
for(var j in q[k]) {
if (q[k].hasOwnProperty(j)) {
var status = q[k][j];
if (!statusMap[status]) {
statusMap[status] = [];
}
statusMap[status].push(j);
}
}
for(j in statusMap) {
ret.push([k, statusMap[j], j]);
}
}
}
return ret;
};
// PageStateBehavior is a re-usable behavior what adds the _state and
// _ctx (page.js context) variables to a Polymer element. All methods are
// implemented as private since they should only be used within a
// Polymer element.
gold.PageStateBehavior = {
properties: {
_state: {
type: Object,
value: function() { return {}; }
}
},
ready: function() {
// Find the status element and listen to corpus changes.
this.async(function() {
this._statusElement = $$$('gold-status-sk');
if (this._statusElement) {
this.listen(this._statusElement, 'corpus-change', '_handleCorpusChange');
}
});
},
_handleCorpusChange: function(ev) {
// Only change anything related to corpus if this element is the
// part of the currently viewed page.
if (Polymer.dom(this).parentNode.hasAttribute('activepage') && this._hasQuery) {
if (this._corpusHome) {
this._redirectHome();
return;
}
var params = sk.query.toParamSet(this._state.query);
params.source_type = [ev.detail];
this._redirectToState({query: sk.query.fromParamSet(params)});
}
},
// _setDefaultState sets the default state (usually reflected in the URL)
// of the is document. 'corpusHome' is a boolean flag that indicates whether
// a corpus change should redirect to the home page.
_setDefaultState: function(defaultState, corpusHome) {
this._defaultState = defaultState;
this._corpusHome = corpusHome;
this._hasQuery = defaultState.hasOwnProperty('query');
},
// _getDefaultStateWithCorpus returns the default search state of this
// element (previously set via _setDefaultState) with the current corpus
// injected.
_getDefaultStateWithCorpus: function(state) {
var ret = state || this._defaultState || {};
if (this._statusElement && this._hasQuery) {
ret = sk.object.shallowCopy(ret);
ret.query = sk.query.fromParamSet({source_type: [this._statusElement.corpus]});
}
return ret;
},
// _initState initializes the '_state' and '_ctx' variables. ctx is the
// context of the page.js route. It creates the value of the _state object
// from the URL query string based on defaultState. It sets the URL to
// the resulting the state.
_initState: function(ctx, defaultState) {
this._ctx = ctx;
this._state = gold.stateFromQuery(defaultState);
if (this._hasQuery) {
this._syncCorpusQuery(defaultState.query);
}
this._setUrlFromState();
},
// _syncCorpusQuery synchronizes the the corpus value between the current
// request (represented by this._state.query) with the corpus in status.
// Effectively changing the corpus in status.
_syncCorpusQuery: function(defaultQueryStr) {
var defaultParams = sk.query.toParamSet(defaultQueryStr);
var params = sk.query.toParamSet(this._state.query);
this._state.query = gold.updateParamsConditionally(params, defaultParams, false);
if (this._statusElement) {
this._statusElement.setCorpus(params.source_type[0]);
}
},
// _redirectToState updates the current state with 'updates'. After it
// saves the current URL to history it redirects (via history.replaceState)
// to newTargetPath, if provided, otherwise it will use the current path.
_redirectToState: function(updates, newTargetPath) {
// Save the current history entry before the redirect.
this._ctx.pushState();
page.redirect(this._getRedirectPath(updates, newTargetPath));
},
// Calculates a new path given the state update and an optional new target
// path.
_getRedirectPath: function(updates, newTargetPath) {
var newState = sk.object.applyDelta(updates, this._state);
var targetPath = newTargetPath || window.location.pathname;
return targetPath + gold.queryFromState(newState);
},
// _getRedirectURL returns a new URL based on the current state and
// target path.
_getRedirectURL: function(updates, newTargetPath) {
var path = this._getRedirectPath(updates, newTargetPath);
var host = window.location.protocol + '//' + window.location.hostname;
return host + ':' + window.location.port + path;
},
// _redirectHome unconditionally redirects to home.
_redirectHome: function() {
this._ctx.pushState();
page.redirect('/' + gold.queryFromState(this._getDefaultStateWithCorpus()));
},
// _replaceState updates the current state with 'updates' and updates
// the URL accordingly. No new page is loaded or reloaded.
_replaceState: function(updates) {
this._state = sk.object.applyDelta(updates, this._state);
this._setUrlFromState();
},
// setUrlFromState simply replaces the query string of the current URL
// with a query string that represents the current state.
_setUrlFromState: function() {
history.replaceState(this._ctx.state, this._ctx.title, window.location.pathname + gold.queryFromState(this._state));
},
};
})();