blob: 42aed8e44928cd819c1e340feca96d897703e4b9 [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="../../third_party/polymer/components/paper-button/paper-button.html">
<link rel="import" href="xhr-element.html">
<polymer-element name="triage-dialog" attributes="xsrfToken alerts">
<template>
<style>
#dialog {
background: white;
padding: 16px 20px;
box-shadow: 0 4px 16px rgba(0, 0, 0, .2);
border: solid 1px #acacac;
border-bottom-color: #999;
margin-bottom: 0.5em;
}
.error {
color: #dd4b39;
font-weight: bold;
}
fieldset {
margin: 0.5em;
border: 1px solid #ebebeb;
}
/* Form controls outside of a fieldset that we want to line up with
controls inside the fieldset. */
.outside-fieldset {
margin-left: 1.75em;
}
.submit-button {
background-color: #4285f4;
color: white;
}
</style>
<form id="dialog" onSubmit="return false;">
<xhr-element id="xhr"></xhr-element>
<template bind if="{{stepTriage}}">
<fieldset name="Triage">
<legend>Triage</legend>
<paper-button class="submit-button" raised on-click="{{fileNewBug}}">New bug</paper-button>
<paper-button class="submit-button" raised on-click="{{associateWithBug}}">Existing bug</paper-button>
<paper-button class="submit-button" raised 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>
<paper-button class="outside-fieldset" raised on-click="{{hide}}">Cancel</paper-button>
</template>
<template bind if="{{stepLoading}}">
<img src="//www.google.com/images/loading.gif">
</template>
<template bind if="{{stepFinished}}">
<template bind if="{{successMessage}}">
{{successMessage}}
</template>
<template bind if="{{errorMessage}}">
<div class="error">There was a problem triaging the bug:</div>
{{errorMessage}}
</template>
<br>
<paper-button class="outside-fieldset" raised on-click="{{hide}}">Cancel</paper-button>
</template>
</form>
</template>
<script>
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.hide();
// 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.hide();
this.fire('triaged', {
'alerts': this.alerts,
'bugid': data['bugid']
});
e.preventDefault();
e.stopPropagation();
}
}.bind(this), true);
},
/**
* Show popup 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/{{bugid}}" target="_blank">' +
'{{bugid}}</a> created/updated. ';
if (data['bisect_error'].length > 0) {
message += '<\ br>Failed to start bisect: {{bisect_error}}';
} else {
message += 'Rietveld issue <a href="{{rietveld_url}}/' +
'{{rietveld_issue_id}}"' +
' target="_blank">{{rietveld_issue_id}}</a>';
}
message = this.template(message, data);
var messageBar = document.getElementById('message-bar');
messageBar.updateContent(message);
},
/**
* 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.setState('triage');
this.bugid = null;
this.errorMessage = null;
this.successMessage = null;
this.checkedNudge = false;
this.$.dialog.style.display = '';
},
/**
* Hides the triage dialog.
*/
hide: function() {
if (this.bugWindow) {
this.bugWindow.close();
}
this.$.dialog.style.display = 'none';
return false;
},
/**
* Updates the this.fileBugKeys based on this.alerts.
*/
alertsChanged: function() {
this.fileBugKeys = [];
if (this.alerts && this.alerts.length) {
this.fileBugKeys = this.alerts.map(function(a) { return a.key; });
}
},
/**
* Opens a window with the /file_bug page, and fills in the form.
*/
fileNewBug: function() {
var details = this.getBugDetails();
var data = [
{name: 'keys', value: this.fileBugKeys.join(',')},
{name: 'summary', value: details.summary},
];
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.fileBugKeys.join(',')}], 'edit_bugs');
},
/**
* Marks an alert as invalid.
*/
markInvalid: function() {
this.changeBugId(-1);
if (!this.errorMessage) {
this.successMessage = 'Alert(s) marked invalid.';
}
},
/**
* Marks an alert as invalid.
*/
markIgnored: function() {
this.changeBugId(-2);
if (!this.errorMessage) {
this.successMessage = 'Alert(s) marked ignored.';
}
},
/**
* Changes the bug ID of the selected alerts.
*/
changeBugId: function(bugId) {
var params = {keys: this.fileBugKeys};
this.bugid = bugId;
params.bug_id = this.bugid;
this.setState('loading');
this.$.xhr.send('/edit_anomalies', this.xsrfToken, params,
function() {
this.fire('triaged', {
'alerts': this.alerts,
'bugid': this.bugid
});
this.setState('finished');
}.bind(this),
function(msg) {
this.errorMessage = msg;
this.setState('finished');
}.bind(this));
},
/**
* Move an alert's position.
*/
nudgeAlert: function() {
var info = this.getNudgedInfo();
var params = {
'keys': this.fileBugKeys,
'new_start_revision': info.startRevision,
'new_end_revision': info.endRevision,
};
this.setState('loading');
this.$.xhr.send('/edit_anomalies', this.xsrfToken, params,
function() {
this.errorMessage = null;
this.fire('alertChangedRevisions', {
'startRev': info.startRevision,
'endRev': info.endRevision,
'newDataIndex': info.dataIndex,
'alerts': this.alerts
});
this.successMessage = 'Alert moved.';
this.setState('finished');
}.bind(this),
function(msg) {
this.errorMessage = msg;
this.setState('finished');
}.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];
},
/**
* Sets the state of the triage dialog.
*
* The state affects the contents of the triage-dialog element through
* affecting conditionally-bound templates. See:
* http://www.polymer-project.org/platform/template.html#if
*
* The possible states are:
* triage: The dialog has been opened and the form is ready.
* loading: Action has just been taken, and we're waiting for response.
* finished: The dialog is finished now,
*
* @param {string} The new state.
*/
setState: function(state) {
this.stepTriage = state == 'triage';
this.stepLoading = state == 'loading';
this.stepFinished = state == 'finished';
},
/**
* Gets information to fill in when filing a new bug.
* @return {Object}
*/
getBugDetails: 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 = ('MIN-MAX CHANGE in TESTSUITE at START:END'
.replace('MIN-MAX', minMax)
.replace('CHANGE', type)
.replace('MASTER', alert.master)
.replace('TESTSUITE', alert.testsuite)
.replace('START', String(startRev))
.replace('END', String(endRev)));
return {
summary: 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;
},
/**
* String substitution using "{{" and "}}" for placeholders.
*
* @param {string} str String template.
* @param {Object} data Dictionary with values to replace.
* @return {string} The replaced string.
*/
template: function(str, data) {
for (var key in data) {
var find = '{{' + key + '}}';
str = str.replace(new RegExp(find, 'g'), data[key]);
}
return str;
},
});
</script>
</polymer-element>