| <!DOCTYPE html> |
| <!-- |
| Copyright 2016 The Chromium Authors. All rights reserved. |
| Use of this source code is governed by a BSD-style license that can be |
| found in the LICENSE file. |
| --> |
| <!-- |
| 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 allows tooltip to show beyond the current window size. |
| // See chart-tooltip.html for more details. |
| this.$.container.sizingTarget = document.querySelector( |
| 'html /deep/ paper-action-dialog::shadow #scroller'); |
| |
| 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> |