blob: 9b386ac695760986a908e6de27851a9a7d1d4876 [file] [log] [blame]
<!--
The res/js/status.js file must be included before this file.
This in an HTML Import-able file this contains the definition
of the following elements:
<commits-data-sk>
This element performs an ajax request to the status backend, parses the json response and
returns bindable properties to be used to render the various components of the status page.
This element takes some filter inputs (i.e. filter and search) and if
either changes, the data will be re-filtered to reflect that.
To use this file import it:
<link href="/res/imp/commits-data-sk.html" rel="import" />
Usage:
<commits-data-sk></commits-data-sk>
Properties:
// inputs
roll_statuses: Array, Status information about the autorollers, as defined in autoroll-widget-sk.html.
commits_to_load: Number, the number of commits to load from the backend
filter: String, the task spec filter to be used.
reload: How often (in seconds) to reload the data.
search: String, the string to be used if filter is "search".
// outputs
branch_heads: Array<Object>, an array of hashes and branch names of the commits.
task_details: Object, a map of commit hash to an object that has the task results by task spec.
task_specs: Object, a map of the task spec names to an object that has, among other things, category, subcategory, comments and master.
tasks: Object, a map of the task spec names to an object that maps task IDs to task results.
categories: Object, a map of the task spec categories to an object that has the subcategories and the colspan (total number of included task specs).
category_list: Array<String>, an array of the task spec category names.
commits: Array<Object>, the commit objects, in chronological order.
commits_map: Object, a map of commit hash to commit objects.
loading: Boolean, if the data is being fetched from the server or parsed.
relanded_map: Object, a map of a commit hash that was relanded to the commit object that relands it
reverted_map: Object, a map of a commit hash that was reverted to the commit object that reverts it
swarming_url: String, the URL of the Swarming server.
task_scheduler_url: String, the URL of the Task Scheduler.
Methods:
forceUpdate: Force the commits-data-sk to reload its data from the server.
forceReProcess: Force the commits-data-sk to re-evaluate its data. Useful for
when Polymer does not automatically trigger updates, eg. for complex objects.
Events:
None.
-->
<link rel="import" href="/res/imp/bower_components/iron-ajax/iron-ajax.html">
<link rel="import" href="/res/common/imp/timer-sk.html">
<dom-module id="commits-data-sk">
<template>
<timer-sk period="[[reload]]" on-trigger="_startUpdate"></timer-sk>
</template>
<script>
(function() {
var VALID_TASK_SPEC_CATEGORIES = ["Build", "Housekeeper", "Infra", "Perf", "Test", "Upload"];
var FILTER_ALL = "all";
var FILTER_DEFAULT = "default";
var FILTER_INTERESTING = "interesting";
var FILTER_FAILURES = "failures";
var FILTER_FAIL_NO_COMMENT = "nocomment";
var FILTER_COMMENTS = "comments";
var FILTER_SEARCH = "search";
var TASK_STATUS_PENDING = "";
var TASK_STATUS_RUNNING = "RUNNING";
var TASK_STATUS_SUCCESS = "SUCCESS";
var TASK_STATUS_FAILURE = "FAILURE";
var TASK_STATUS_MISHAP = "MISHAP";
var TIME_POINTS = [
{
label:"-1h",
offset: 60 * 60 * 1000,
},
{
label:"-3h",
offset: 3 * 60 * 60 * 1000,
},
{
label:"-1d",
offset: 24* 60 * 60 * 1000,
},
];
// shortCommit returns the first 7 characters of a commit hash.
function shortCommit(commit) {
return commit.substring(0, 7);
}
// shortAuthor shortens the commit author field by returning the
// parenthesized email address if it exists. If it does not exist, the
// entire author field is used.
function shortAuthor(author) {
var re = /.*\((.+)\)/;
var match = re.exec(author);
var res = author;
if (match) {
res = match[1];
}
return res.split("@")[0];
}
// shortSubject truncates a commit subject line to 72 characters if needed.
// If the text was shortened, the last three characters are replaced by
// ellipsis.
function shortSubject(subject) {
return sk.truncate(subject, 72);
}
// findIssueAndReviewTool returns an object literal of the form
// {issue, patchStorage}. patchStorage will be either Gerrit or empty, and
// issue will be the CL number or empty.
// If an issue cannot be determined then an empty string is returned for
// both issue and patchStorage.
function findIssueAndReviewTool(commit) {
// See if it is a Gerrit CL.
var gerritRE = /(.|[\r\n])*Reviewed-on:.*\/([0-9]*)/g;
var gerritTokens = gerritRE.exec(commit.body);
if (gerritTokens) {
return {
issue: gerritTokens[gerritTokens.length - 1],
patchStorage: 'gerrit'
};
}
// Could not find a CL number return an empty string.
return {
issue: '',
patchStorage: ''
};
}
function isGerritIssue(commit) {
return commit.patchStorage === 'gerrit';
}
// Find and return the commit which was reverted by the given commit.
function findRevertedCommit(commits, commit) {
patt = new RegExp("^This reverts commit ([a-f0-9]+)");
var tokens = patt.exec(commit.body);
if (tokens) {
return commits[tokens[tokens.length - 1]];
}
return null;
}
// Find and return the commit which was relanded by the given commit.
function findRelandedCommit(commits, commit) {
// Relands can take one of two formats. The first is a "direct" reland.
patt = new RegExp("^This is a reland of ([a-f0-9]+)");
var tokens = patt.exec(commit.body);
if (tokens) {
return commits[tokens[tokens.length - 1]];
}
// The second is a revert of a revert.
var revert = findRevertedCommit(commits, commit);
if (revert) {
return findRevertedCommit(commits, revert);
}
return null;
}
function getTaskColorClass(task) {
// These styles are from the styles-sk module.
if (task.status == TASK_STATUS_PENDING || task.status == TASK_STATUS_RUNNING) {
return "bg-inprogress";
} else if (task.status == TASK_STATUS_SUCCESS) {
return "bg-success";
} else if (task.status == TASK_STATUS_FAILURE) {
return "bg-failure";
} else if (task.status == TASK_STATUS_MISHAP) {
return "bg-exception";
}
console.log("Unknown color for task "+task);
return "bg-inprogress";
}
Polymer({
is: "commits-data-sk",
properties: {
// input only
roll_statuses: {
type: Array,
value: function() {
return [];
},
},
commits_to_load: {
type: Number,
value: 35,
observer: "_reloadFromScratch",
},
filter: {
type: String,
observer:"_filterTaskSpecs",
},
is_sheriff: {
type: Boolean,
},
is_trooper: {
type: Boolean,
},
reload: {
type: Number,
value: 60,
},
repo: {
type: String,
observer: "_reloadFromScratch",
},
search: {
type: String,
observer: "_searchChanged",
},
// output only
branch_heads: {
type: Array,
value: function() {
return [];
},
notify:true,
},
task_details: {
type: Object,
value: function() {
return {};
},
notify:true,
},
tasks: {
type: Object,
value: function() {
return {};
},
notify:true,
},
task_specs: {
type: Object,
value: function() {
return {};
},
notify:true,
},
categories: {
type: Object,
value: function() {
return {};
},
notify:true,
},
category_list: {
type: Array,
value: function() {
return [];
},
notify:true,
},
commits: {
type: Array,
notify:true,
},
commits_map: {
type: Object,
notify:true,
},
loading: {
type: Number,
notify: true,
computed: "_or(_activeAJAX, _filtering)",
},
purple_tasks: {
type: Array,
notify: true,
},
relanded_map: {
type: Object,
value: function() {
return {};
},
notify:true,
},
reverted_map: {
type: Object,
value: function() {
return {};
},
notify:true,
},
swarming_url: {
type: String,
notify: true,
},
task_scheduler_url: {
type: String,
notify: true,
},
time_points: {
type: Object,
value: function() {
return {};
},
notify:true,
},
//private
_activeAJAX: {
type: Boolean,
value: false,
},
_filtering: {
type: Boolean,
value: false,
},
_data: {
type: Object,
value: function() {
return {
branch_heads: [],
commitComments: {},
commits: [],
tasks: {},
tasksByCommit: {},
taskComments: {},
tasks: {},
taskSpecComments: {},
};
},
observer:"",
},
_last_load_ts: {
type: Number,
value: 0,
},
_server_pod_id: {
type: String,
value: "",
},
_updateRequested: {
type: Boolean,
value: false,
},
_updateFromScratch: {
type: Boolean,
value: false,
},
},
observers: [
"_processData(_data.*, roll_statuses.*)",
],
_or: function(a,b) {
return a || b;
},
_makeCommitsMap: function(arr) {
if (!arr || arr.length == 0) {
this.set("commits_map", {});
return;
}
var m = {};
arr.forEach(function(c){
m[c.hash] = c;
});
this.set("commits_map", m);
},
_clearData: function() {
this._data = {
branch_heads: [],
commitComments: {},
commits: [],
taskComments: {},
tasks: {},
tasksByCommit: {},
taskSpecComments: {},
};
},
_reloadFromScratch: function() {
this._startUpdate(true);
},
_startUpdate: function(fromScratch) {
console.log("_startUpdate(" + fromScratch + ")");
if (this._activeAJAX) {
this._updateRequested = true;
if (fromScratch === true) {
this._updateFromScratch = true;
}
return;
}
this.set("_activeAJAX", true);
this._update(fromScratch === true);
},
_updateFinished: function() {
console.log("_updateFinished");
if (this._updateRequested) {
var fromScratch = this._updateFromScratch;
this._updateRequested = false;
this._updateFromScratch = false;
this._update(fromScratch);
} else {
this._updateRequested = false;
this._updateFromScratch = false;
this.set("_activeAJAX", false);
}
},
_update: function(fromScratch) {
console.log("_update("+ fromScratch + ")");
// Sanity check.
if (!this._activeAJAX) {
throw "Cannot call _update directly; must use _startUpdate!";
}
if (!this.repo || !this.commits_to_load) {
this._updateFinished();
return;
}
var ts = Date.now();
var url = "/json/" + this.repo + "/incremental?n=" + this.commits_to_load;
if (this._last_load_ts && !fromScratch) {
url += "&from=" + this._last_load_ts;
}
if (this._server_pod_id) {
url += "&pod=" + this._server_pod_id;
}
sk.get(url).then(JSON.parse).then(function(json) {
// Clear out the existing data if necessary.
if (json.start_over || fromScratch) {
this._clearData();
}
// Keep track of the server pod ID. This ensures that we're in sync
// with the server.
this._server_pod_id = json.pod;
// Mix the new data into the existing.
if (!json.commits) {
json.commits = [];
}
var sliceIdx = this._data.commits.length - json.commits.length;
var keep = this._data.commits.slice(0, sliceIdx);
var remove = this._data.commits.slice(sliceIdx, this._data.commits.length);
this._data.commits = json.commits.concat(keep);
// Replace the branch heads if they've changed.
if (json.branch_heads) {
this._data.branch_heads = json.branch_heads;
}
// Add new tasks, replace modified ones.
if (json.tasks) {
for (var i = 0; i < json.tasks.length; i++) {
var task = json.tasks[i];
this._data.tasks[task.id] = task;
}
}
// Remove too-old tasks.
for (var i = 0; i < remove.length; i++) {
var commit = remove[i];
for (var id in this._data.tasks) {
if (this._data.tasks[id].revision == commit) {
delete this._data.tasks[id];
}
}
}
// Map commits to tasks.
for (var id in this._data.tasks) {
var task = this._data.tasks[id];
if (task.commits) {
for (var j = 0; j < task.commits.length; j++) {
var commit = task.commits[j];
var tasksForCommit = this._data.tasksByCommit[commit];
if (!tasksForCommit) {
tasksForCommit = {};
this._data.tasksByCommit[commit] = tasksForCommit;
}
tasksForCommit[task.name] = task;
}
}
}
if (json.swarming_url) {
this.set("swarming_url", json.swarming_url);
}
if (json.task_scheduler_url) {
this.set("task_scheduler_url", json.task_scheduler_url);
}
if (json.commit_comments) {
this._data.commitComments = json.commit_comments;
}
if (json.task_comments) {
this._data.taskComments = json.task_comments;
}
if (json.task_spec_comments) {
this._data.taskSpecComments = json.task_spec_comments;
}
// Update the last-load timestamp.
this._last_load_ts = ts;
this._updateFinished();
this._processData(this._data);
}.bind(this)).catch(function(msg) {
this._updateFinished();
sk.errorMessage("Failed to load new data: " + msg);
}.bind(this));
},
_processData: function(data) {
if (!data || !data.commits || data.commits.length == 0) {
return;
}
this.set("_filtering", true);
console.time("_processData");
for (var i = 0; i < data.commits.length; i++) {
data.commits[i].comments = data.commitComments[data.commits[i].hash] || [];
}
var commits = data.commits;
var commitsMap = {};
for (var i = 0; i < commits.length; i++) {
commitsMap[commits[i].hash] = commits[i];
}
// Prepare task data.
var tasks = {};
var task_specs = {};
var task_details = this.task_details;
for (var commit in data.tasksByCommit) {
task_details[commit] = data.tasksByCommit[commit];
for (var taskSpec in task_details[commit]) {
var task = task_details[commit][taskSpec];
task.comments = [];
if (data.taskComments[commit] && data.taskComments[commit][taskSpec]) {
task.comments = data.taskComments[commit][taskSpec];
}
}
}
for (var i = 0; i < commits.length; i++) {
var commit = commits[i];
commit.shortAuthor = shortAuthor(commit.author);
commit.shortHash = shortCommit(commit.hash);
commit.shortSubject = shortSubject(commit.subject);
var c = findIssueAndReviewTool(commit);
commit.issue = c.issue;
commit.patchStorage = c.patchStorage;
}
for (var i = 0; i < commits.length; i++) {
var commit = commits[i];
commit.isRevert = false;
var reverted = findRevertedCommit(commitsMap, commit);
if (reverted) {
commit.isRevert = true;
this.reverted_map[reverted.hash] = commit;
}
commit.isReland = false;
var relanded = findRelandedCommit(commitsMap, commit);
if (relanded) {
commit.isReland = true;
this.relanded_map[relanded.hash] = commit;
}
}
for (var i = 0; i < commits.length; i++) {
var commit = commits[i];
commit.ignoreFailure = commit.comments &&
commit.comments.length > 0 &&
commit.comments[commit.comments.length-1].ignoreFailure;
if (this.reverted_map[commit.hash]) {
commit.ignoreFailure = true;
}
commit.displayClass = {};
if (!task_details[commit.hash]) {
task_details[commit.hash] = {};
}
for (var taskSpec in task_details[commit.hash]) {
var task = task_details[commit.hash][taskSpec];
task.colorClass = getTaskColorClass(task);
if (!tasks[taskSpec]) {
// This is the first time we've seen this task spec.
tasks[taskSpec] = {};
var taskSpecDetails = {
"comments": data.taskSpecComments[taskSpec] || [],
"name": taskSpec,
// We're traveling backward in time, so the first task we
// find for a given task spec is its most recent.
"colorClass": task.colorClass,
"flaky": false,
"ignoreFailure": false,
};
var split = taskSpec.split("-");
if (split.length >= 2 && VALID_TASK_SPEC_CATEGORIES.indexOf(split[0]) != -1) {
taskSpecDetails.category = split[0];
taskSpecDetails.subcategory = split[1];
}
if (taskSpecDetails.comments && taskSpecDetails.comments.length > 0) {
taskSpecDetails.flaky = !!taskSpecDetails.comments[taskSpecDetails.comments.length-1].flaky;
taskSpecDetails.ignoreFailure = !!taskSpecDetails.comments[taskSpecDetails.comments.length-1].ignoreFailure
}
task_specs[taskSpec] = taskSpecDetails;
}
tasks[taskSpec][task.id] = task;
// Figure out the display class to use.
var classes = [CLASS_TASK_SINGLE];
if (i > 0) {
// We are drawing from most recent on back in time. prevCommit is really the "next"
// commit in a temporal timeline. But, it was the previously drawn commit, so the
// name sticks.
var prevCommit = commits[i-1];
var prevDetails = task_details[prevCommit.hash] || {};
if (prevCommit.parent.indexOf(commit.hash) === -1) {
// We skipped one or more commits. This is likely due to a branch. We need to find the last drawn commit whose parent is this one.
prevCommit = undefined;
for (var j = i-1; j>= 0; j--) {
if (commits[j].parent.indexOf(commit.hash) !== -1) {
prevCommit = commits[j];
break;
}
}
if (prevCommit) {
// If the previously drawn commit does not exist, it basically means we are the
// head of the branch. If it does exist, we change it to have a dashed bottom
// and for this commit to have a dashed top.
prevDetails = task_details[prevCommit.hash] || {};
var prevTask = prevDetails[taskSpec];
// Only continue drawing if it's actually the same task
if (prevTask && prevTask.id == task.id) {
classes = [CLASS_TASK_BOTTOM, CLASS_DASHED_TOP];
if (prevCommit.displayClass[taskSpec].indexOf(CLASS_TASK_SINGLE) >= 0) {
prevCommit.displayClass[taskSpec] = [CLASS_TASK_TOP, CLASS_DASHED_BOTTOM];
} else {
prevCommit.displayClass[taskSpec] = [CLASS_TASK_MIDDLE, CLASS_DASHED_BOTTOM];
}
}
}
} else if (prevDetails) {
var prevTask = prevDetails[taskSpec];
// Only continue drawing if it's actually the same task
if (prevTask && prevTask.id == task.id) {
classes = [CLASS_TASK_BOTTOM];
var prevClasses = prevCommit.displayClass[taskSpec];
if (prevClasses.indexOf(CLASS_TASK_SINGLE) >= 0) {
prevCommit.displayClass[taskSpec] = [CLASS_TASK_TOP];
} else if (prevClasses.indexOf(CLASS_TASK_BOTTOM) >= 0) {
var j = prevClasses.indexOf(CLASS_TASK_BOTTOM);
prevClasses[j] = CLASS_TASK_MIDDLE;
prevCommit.displayClass[taskSpec] = prevClasses;
}
}
}
}
commit.displayClass[taskSpec] = classes;
}
}
this._makeCommitsMap(commits);
this.set("tasks", tasks);
this.set("task_details", task_details);
this.set("task_specs", task_specs);
this._filterTaskSpecs();
// Add autoroll tags as branch heads.
var filteredBranchHeads = [];
for (var i = 0; i < data.branch_heads.length; i++) {
var b = data.branch_heads[i];
filteredBranchHeads.push(b);
}
for (var i = 0; i < this.roll_statuses.length; i++) {
var roll = this.roll_statuses[i];
if (roll.lastRollRev) {
filteredBranchHeads.push({
name: roll.name + " rolled",
head: roll.lastRollRev,
});
}
if (roll.currentRollRev) {
filteredBranchHeads.push({
name: roll.name + " rolling",
head: roll.currentRollRev,
});
}
}
this.set("branch_heads", filteredBranchHeads);
var timeIdx = 0;
var now = new Date();
var time_points = {};
// If the first commit happened after our first time point cutoff, we advance past it.
while ((timeIdx < TIME_POINTS.length) && (now - TIME_POINTS[timeIdx].offset) > new Date(commits[0].timestamp)) {
timeIdx++;
}
// Going backwards in time, we place a marker if the current commit happened before the time offset and the following commit happened after. Once we find a cutoff, start looking for the next time point.
var commitIdx = 0;
while (commitIdx < (commits.length - 1) && timeIdx < TIME_POINTS.length) {
var c = commits[commitIdx];
var curr = new Date(c.timestamp);
var next = new Date(commits[commitIdx+1].timestamp);
if ((now - TIME_POINTS[timeIdx].offset) <= curr && (now - TIME_POINTS[timeIdx].offset) > next) {
time_points[c.hash] = TIME_POINTS[timeIdx];
timeIdx++;
// We don't increment commitIdx because we want to double check the current cutoff.
// Example: commit A happened 59 minutes ago and commit B happened 1.3 days ago.
// The time point between them should be the -1d one, not the -1h one. Since time_points
// is based off of commit, we can recheck and replace the shorter cutoffs if necessary.
} else {
commitIdx++;
}
}
// Check for the last commit as well, except we don't compare it to the following commit.
var last = commits[commits.length - 1];
if ((timeIdx < TIME_POINTS.length) && (now - TIME_POINTS[timeIdx].offset) <= last.timestamp) {
time_points[last.hash] = TIME_POINTS[timeIdx];
}
console.timeEnd("_processData");
this.set("_filtering", false);
// Actually draw the commits.
this.set("commits", commits);
this.set("time_points", time_points);
},
// Apply the desired filter to the tasks.
_filterTaskSpecs: function() {
console.time("filterTaskSpecs");
var filteredTaskSpecs = [];
var selected = this.filter || FILTER_DEFAULT;
if (selected === FILTER_DEFAULT) {
if (this.is_sheriff || this.is_trooper) {
selected = FILTER_FAILURES;
} else {
selected = FILTER_INTERESTING;
}
}
if (selected == FILTER_ALL) {
for (var taskSpec in this.task_specs) {
filteredTaskSpecs.push(taskSpec);
}
} else if (selected == FILTER_INTERESTING || selected == FILTER_FAILURES || selected == FILTER_FAIL_NO_COMMENT) {
for (var taskSpec in this.task_specs) {
var failed = false;
var succeeded = false;
for (var taskId in this.tasks[taskSpec]) {
var task = this.tasks[taskSpec][taskId];
if (task.status == TASK_STATUS_PENDING || task.status == TASK_STATUS_RUNNING) {
continue;
}
// If interesting or "failing w/o comment" is selected, compute ignoreFailure
// and skip this task if it belongs entirely to commits that have been ignored.
if (selected === FILTER_INTERESTING || selected === FILTER_FAIL_NO_COMMENT) {
var commits = task.commits || [];
var isIgnored = commits.length > 0;
commits.forEach(function(c){
var o = this.commits_map[c];
isIgnored = isIgnored && o && o.ignoreFailure;
}.bind(this));
if (isIgnored) {
continue;
}
}
if (task.status == TASK_STATUS_SUCCESS) {
succeeded = true;
} else {
failed = true;
}
if (selected == FILTER_INTERESTING) {
if (succeeded && failed && !this.task_specs[taskSpec].ignoreFailure) {
filteredTaskSpecs.push(taskSpec);
break;
}
} else if (selected == FILTER_FAILURES) {
if (failed) {
filteredTaskSpecs.push(taskSpec);
break;
}
} else if (selected == FILTER_FAIL_NO_COMMENT) {
if (task.status != TASK_STATUS_SUCCESS && (!this.task_specs[taskSpec].comments || this.task_specs[taskSpec].comments.length == 0)) {
if (!task.comments || task.comments.length == 0) {
filteredTaskSpecs.push(taskSpec);
break;
}
}
}
}
}
} else if (selected == FILTER_COMMENTS) {
for (var taskSpec in this.task_specs) {
if (this.task_specs[taskSpec].comments && this.task_specs[taskSpec].comments.length > 0) {
filteredTaskSpecs.push(taskSpec);
continue;
}
for (var taskId in this.tasks[taskSpec]) {
var task = this.tasks[taskSpec][taskId];
if (task.status == TASK_STATUS_PENDING || task.status == TASK_STATUS_RUNNING) {
continue;
}
if (task.comments && task.comments.length > 0) {
filteredTaskSpecs.push(taskSpec);
break;
}
}
}
} else if (selected == FILTER_SEARCH) {
var matchText = this.search;
for (var taskSpec in this.task_specs) {
if (taskSpec.toLowerCase().match(matchText.toLowerCase())) {
filteredTaskSpecs.push(taskSpec);
}
}
} else {
console.error("Invalid task spec filter selection: " + selected);
}
sk.sortStrings(filteredTaskSpecs);
var categories = {};
var categoryList = [];
var purpleTasks = [];
for (var i = 0; i < filteredTaskSpecs.length; i++) {
var taskSpecName = filteredTaskSpecs[i];
var category = this.task_specs[taskSpecName].category;
if (!category) {
category = "Other";
}
if (!categories[category]) {
categories[category] = {
colspan: 0,
subcategoryList: [],
subcategories: {},
};
categoryList.push(category);
}
var subcategory = this.task_specs[taskSpecName].subcategory;
if (!subcategory) {
subcategory = "Other";
}
if (!categories[category].subcategories[subcategory]) {
categories[category].subcategories[subcategory] = {
task_specs: [],
};
categories[category].subcategoryList.push(subcategory);
}
categories[category].subcategories[subcategory].task_specs.push(taskSpecName);
categories[category].colspan++;
// Find any purple tasks for this task spec.
// Exclude the Google3-Autoroller since it is not a real bot and
// therefore cannot be retried.
if (taskSpecName != "Google3-Autoroller") {
for (var taskId in this.tasks[taskSpecName]) {
var task = this.tasks[taskSpecName][taskId];
if (task.status == TASK_STATUS_MISHAP) {
// TODO(borenet): This is a bit of a hack.
// Jobs are named after task, test, or perf tasks, but not
// uploads. If this is an upload, trim the prefix.
var jobName = taskSpecName;
if (jobName.startsWith("Upload-")) {
jobName = jobName.substring("Upload-".length);
}
purpleTasks.push({"name": jobName, "commit": task.revision});
}
}
}
}
this.set("categories", categories);
this.set("category_list", categoryList);
this.set("purple_tasks", purpleTasks);
console.timeEnd("filterTaskSpecs");
},
_searchChanged: function() {
// This callback fires every time the user presses a key inside the
// input box. Updating the display can be really slow when there are
// a lot of task specs, so we should wait until the user is done typing
// before re-filtering.
if (this.taskSpecSearchChangedTimeout) {
window.clearTimeout(this.taskSpecSearchChangedTimeout);
}
this.taskSpecSearchChangedTimeout = window.setTimeout(function() {
this.taskSpecSearchChangedTimeout = null;
// If "search" is already selected, filter the task specs.
if (this.filter == "search") {
this._filterTaskSpecs();
}
}.bind(this), 400);
},
forceUpdate: function() {
this._startUpdate(false);
},
forceReProcess: function() {
this._processData(this._data, this.roller_statuses);
},
});
})();
</script>
</dom-module