blob: e7d9c0127d8c0abab2bbc0434ca6dbb011706553 [file] [log] [blame]
<!--
The triage-dialog element is the dialog box that is shown when a user clicks
on an alert, or clicks on a "triage" button on the alerts page. It allows the
-->
<link rel="import" href="/components/paper-button/paper-button.html">
<link rel="import" href="/components/paper-dialog/paper-action-dialog.html">
<link rel="import" href="/dashboard/elements/base-form.html">
<link rel="import" href="/dashboard/static/simple_xhr.html">
<polymer-element name="triage-dialog" extends="base-form"
attributes="xsrfToken alerts">
<template>
<style>
#container {
position: relative;
margin: 0 0 10px 0;
}
</style>
<paper-action-dialog id="container" layered="false">
<!-- Styling for paper-action-dialog's children. -->
<style>
#loading {
background-color: white;
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
justify-content: center;
-webkit-justify-content: center;
opacity: 0;
transition: opacity 0.2s;
}
paper-button[affirmative] {
background: #4285f4;
color: #fff;
}
fieldset {
border: 1px solid #ebebeb;
}
.submit-button {
background-color: #4285f4;
color: white;
}
form {
margin-bottom: 0;
}
</style>
<form>
<fieldset name="Triage">
<legend>Triage</legend>
<paper-button class="submit-button" raised affirmative
on-click="{{fileNewBug}}">New bug</paper-button>
<paper-button class="submit-button" raised affirmative
on-click="{{associateWithBug}}">Existing bug</paper-button>
<paper-button class="submit-button" raised affirmative
on-click="{{markIgnored}}">Ignore</paper-button>
</fieldset>
<fieldset name="Fix-up alert">
<legend>Fix-up</legend>
<select class="kennedy-button" id="nudgeSelect" hidden?="{{!nudgeList}}" on-change="{{nudgeAlert}}">
<option template repeat="{{nudgeList}}" value="{{endRevision}}" selected?="{{selected}}">
Nudge {{amount}} {{displayEndRevision}} ({{value}})
</option>
</select>
<paper-button raised on-click="{{markInvalid}}">Invalid</paper-button>
</fieldset>
</form>
<paper-button dismissive raised>Close</paper-button>
<template if="{{loading}}">
<div id="loading">
<paper-spinner active></paper-spinner>
</div>
</template>
</paper-action-dialog>
</template>
<script>
'use strict';
Polymer('triage-dialog', {
// A string describing the magnitude of change from zero to non-zero.
FREAKIN_HUGE: 'zero-to-nonzero',
/**
* Custom element lifecycle callback.
* Called when the triage-dialog element is ready.
*/
ready: function() {
this.bugWindow = null;
this.close();
// Event listener for the message event.
// The message event can be fired when another window, such as the
// create bug dialog window, uses window.postMessage().
// This is used so that the triage dialog is hidden when the popup
// window is shown.
window.addEventListener('message', function(e) {
if (!e || !e.data) {
return;
}
var data = JSON.parse(e.data);
if (data['action'] == 'bug_create_result') {
if (this.bugWindow) {
this.bugWindow.close();
}
this.showCreatedBugResult(data);
this.close();
this.fire('triaged', {
'alerts': this.alerts,
'bugid': data['bug_id']
});
e.preventDefault();
e.stopPropagation();
}
}.bind(this), true);
},
/**
* Shows a pop-up message for filed bug result.
*
* @param {object} dataObject with info about filed bug set from
* bug_result.html.
*/
showCreatedBugResult: function(data) {
var message = ('Bug <a href="http://crbug.com/{{bug_id}}" ' +
'target="_blank">{{bug_id}}</a> created/updated. ');
if (data['issue_id']) {
message += ('Bisect job <a href="{{issue_url}}" target="_blank">' +
'{{issue_id}}</a> started.');
}
if (data['bisect_error']) {
message += 'No bisect automatically started. {{bisect_error}}';
}
this.showMessage(message, data);
},
/**
* Initializes and displays the triage dialog.
*/
show: function() {
if (this.alerts && this.alerts.length && this.alerts[0].nudgeList) {
this.nudgeList = this.alerts[0].nudgeList;
} else {
this.nudgeList = null;
}
this.open();
this.bugid = null;
},
/**
* Updates the this.alertKeys based on this.alerts.
*/
alertsChanged: function() {
this.alertKeys = [];
if (this.alerts && this.alerts.length) {
this.alertKeys = this.alerts.map(function(a) { return a.key; });
}
},
/**
* Opens a window with the /file_bug page, and fills in the form.
*/
fileNewBug: function() {
var bugTitle = this.getBugTitle();
var data = [
{name: 'keys', value: this.alertKeys.join(',')},
{name: 'summary', value: bugTitle}
];
this.bugWindow = this.openAndPost('/file_bug', data, 'edit_bugs');
},
/**
* Opens a window with the /associate_alerts page, which allows the user
* to associate an alert with an existing issue.
*/
associateWithBug: function() {
this.bugWindow = this.openAndPost('/associate_alerts',
[{name: 'keys', value: this.alertKeys.join(',')}], 'edit_bugs');
},
/**
* Marks an alert as invalid.
*/
markInvalid: function() {
this.changeBugId(-1, 'Alert(s) marked invalid.');
},
/**
* Marks an alert as invalid.
*/
markIgnored: function() {
this.changeBugId(-2, 'Alert(s) marked ignored.');
},
/**
* Changes the bug ID of the selected alerts.
*/
changeBugId: function(bugId, successMessage) {
var params = {
keys: this.alertKeys,
xsrf_token: this.xsrfToken
};
this.bugid = bugId;
params.bug_id = this.bugid;
this.loading = true;
simple_xhr.send('/edit_anomalies', params,
function() {
this.fire('triaged', {
'alerts': this.alerts,
'bugid': this.bugid
});
this.loading = false;
if (successMessage) {
this.showMessage(successMessage);
}
}.bind(this),
function(msg) {
this.showErrorMessage('There was a problem triaging the bug',
msg);
this.loading = false;
}.bind(this));
},
/**
* Move an alert's position.
*/
nudgeAlert: function() {
var info = this.getNudgedInfo();
var params = {
'keys': this.alertKeys,
'new_start_revision': info.startRevision,
'new_end_revision': info.endRevision,
'xsrf_token': this.xsrfToken
};
this.loading = true;
simple_xhr.send('/edit_anomalies', params,
function() {
this.fire('alertChangedRevisions', {
'startRev': info.startRevision,
'endRev': info.endRevision,
'newDataIndex': info.dataIndex,
'alerts': this.alerts
});
this.showMessage('Alert moved.');
this.loading = false;
}.bind(this),
function(msg) {
this.showErrorMessage('There was a problem triaging the bug',
msg);
this.loading = false;
}.bind(this));
},
/**
* Gets the item in the nudge list that was selected.
* Note that this.nudgeList is populated in chart-container.
*/
getNudgedInfo: function() {
var index = this.shadowRoot.getElementById('nudgeSelect').selectedIndex;
return this.nudgeList[index];
},
/**
* Returns a default bug title to use when filing a new bug.
* @return {string} The bug title.
*/
getBugTitle: function() {
if (this.alerts[0].percent_changed) {
return this.getBugTitleForAnomaly();
} else {
return this.getBugTitleForStoppageAlert();
}
},
getBugTitleForAnomaly: function() {
var type = 'improvement';
var percentMin = Infinity;
var percentMax = -Infinity;
var maxRegressionFound = false;
var startRev = Infinity;
var endRev = -Infinity;
for (var i = 0; i < this.alerts.length; i++) {
var alert = this.alerts[i];
if (!alert.improvement) {
type = 'regression';
}
var percent = Infinity;
if (alert.percent_changed == this.FREAKIN_HUGE &&
!maxRegressionFound) {
maxRegressionFound = true;
}
else
percent = Math.abs(parseFloat(alert.percent_changed));
if (percent < percentMin) {
percentMin = percent;
}
if (percent > percentMax) {
percentMax = percent;
}
if (alert.start_revision < startRev) {
startRev = alert.start_revision;
}
if (alert.end_revision > endRev) {
endRev = alert.end_revision;
}
}
// Round the percentages to 1 decimal place.
percentMin = Math.round(percentMin * 10) / 10;
percentMax = Math.round(percentMax * 10) / 10;
var minMax = percentMin + '%-' + percentMax + '%';
if (maxRegressionFound) {
if (percentMin == Infinity) {
// Both percentMin and percentMax were at Infinity.
// Record a "freakin' huge" (TM) regression.
minMax = 'A ' + this.FREAKIN_HUGE;
}
else {
// Regressions ranged from Infinity to some other lower percentage.
minMax = 'A ' + this.FREAKIN_HUGE + ' to ' + percentMin + '%';
}
}
else if (percentMin == percentMax) {
minMax = percentMin + '%';
}
var alert = this.alerts[0];
var summary = this.template(
'{{range}} {{type}} in {{suite}} at {{start}}:{{end}}',
{
'range': minMax,
'type': type,
'suite': alert.testsuite,
'start': String(startRev),
'end': String(endRev)
});
return summary;
},
getBugTitleForStoppageAlert: function() {
var alert = this.alerts[0];
var summary = this.template(
'No data received for {{suite}} from {{bot}} since {{rev}}',
{
'suite': alert.testsuite,
'bot': alert.bot,
'rev': alert.end_revision
});
return summary;
},
/**
* Opens a new window and post data.
*
* GET request has limited URL length. This method allows us to send a
* POST request and receive the response on a new window.
*
* @param {string} url URL endpoint to post to.
* @param {Array.<Object>} data Array of objects with have name and
* value properties. This is the data to post.
* @param {string} target Window name.
* @return {Object} The window that was opened.
*/
openAndPost: function(url, data, target) {
var popup = window.open('about:blank', target, 'width=600,height=480');
var form = document.createElement('form');
form.action = url;
form.method = 'POST';
form.target = target;
if (data) {
for (var i = 0; i < data.length; i++) {
var input = document.createElement('textarea');
input.name = data[i].name;
input.value = (typeof data[i].value === 'object' ?
JSON.stringify(data[i].value) : data[i].value);
form.appendChild(input);
}
}
form.style.display = 'none';
document.body.appendChild(form);
form.submit();
return popup;
},
open: function() {
this.$.container.open();
},
close: function() {
this.$.container.close();
}
});
</script>
</polymer-element>