| <!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. |
| --> |
| |
| <link rel="import" href="/components/paper-button/paper-button.html"> |
| |
| <link rel="import" href="/dashboard/elements/bisect-status.html"> |
| <link rel="import" href="/dashboard/elements/bug-info-span.html"> |
| <link rel="import" href="/dashboard/elements/revision-range.html"> |
| <link rel="import" href="/dashboard/elements/triage-dialog.html"> |
| <link rel="import" href="/dashboard/static/uri.html"> |
| |
| <polymer-element name="alerts-table" |
| attributes="sortBy sortDirection |
| xsrfToken alertList extraColumns"> |
| <template> |
| <style> |
| #alerts { |
| border-collapse: collapse; |
| border-spacing: 0; |
| font-size: small; |
| table-layout: fixed; |
| width: 100%; |
| } |
| |
| #alerts thead { |
| cursor: pointer; |
| } |
| |
| #alerts thead th { |
| font-weight: bold; |
| text-align: left; |
| } |
| |
| #alerts thead th, |
| #alerts thead td { |
| border-bottom: 1px solid #8c8b8b; |
| padding: 10px; |
| } |
| |
| #alerts thead th:active, |
| #alerts thead td:active { |
| outline: none; |
| } |
| |
| #alerts #groupheader { |
| padding: 3px; |
| width: 23px; |
| } |
| |
| #alerts #checkheader, #alerts #graphheader { |
| padding: 0; |
| width: 30px; |
| } |
| |
| #alerts #bug_id { |
| width: 75px; |
| } |
| |
| #alerts #end_revision, #alerts #master { |
| width: 100px; |
| } |
| |
| #alerts #percent_changed { |
| text-align: right; |
| width: 50px; |
| } |
| |
| #alerts tbody tr { |
| background-color: white; |
| height: 26px; |
| } |
| |
| #alerts tbody tr.selected { |
| background-color: #b0bed9; |
| } |
| |
| #alerts tbody td { |
| padding: 3px 5px 3px 5px; |
| position: relative; |
| word-wrap: break-word; |
| } |
| |
| #alerts tbody td:first-child { |
| text-align: center; |
| padding-right: 3px; |
| padding-left: 3px; |
| } |
| |
| #alerts tbody th, #alerts tbody td { |
| border-bottom: 1px solid #ddd; |
| } |
| |
| #alerts tbody tr:first-child th, |
| #alerts tbody tr:first-child td { |
| border-top: none; |
| } |
| |
| #alerts tbody tr:not(.group-member):hover { |
| background-color: whitesmoke; |
| } |
| |
| #alerts tbody tr.group-member:hover > td:not(:first-child) { |
| background-color: whitesmoke; |
| } |
| |
| #alerts tbody tr.group-member td { |
| border-bottom: none; |
| } |
| |
| #alerts tbody tr td:first-child, #alerts thead th:first-child { |
| border-right: 1px solid transparent; |
| } |
| |
| #alerts tbody tr.group-member td:first-child { |
| border-bottom: 0px solid #ddd; |
| border-right: 1px solid #ddd; |
| } |
| |
| #alerts tbody tr.group-member+tr.group-header td { |
| border-top: 1px solid #ddd; |
| } |
| |
| #alerts tbody tr[expanded] td:not(:first-child) { |
| border-bottom: none; |
| } |
| |
| #alerts tbody td:last-child, #alerts thead th:last-child { |
| padding: 0; |
| } |
| |
| th[data-sort-direction=down]::after { |
| content: " ▼"; |
| } |
| |
| th[data-sort-direction=up]::after { |
| content: " ▲"; |
| } |
| |
| .percent_changed { |
| color: #a00; |
| width: 70px; |
| text-align: right; |
| word-wrap: break-word; |
| } |
| |
| tr[improvement] .percent_changed { |
| color: #0a0; |
| } |
| |
| /* Checkboxes */ |
| input[type=checkbox]:checked::after { |
| font-size: 1.3em; |
| content: "✓"; |
| position: absolute; |
| top: -5px; |
| left: -1px; |
| } |
| |
| input[type=checkbox]:focus { |
| outline: none; |
| border-color: #4d90fe; |
| } |
| |
| input[type=checkbox] { |
| -webkit-appearance: none; |
| width: 13px; |
| height: 13px; |
| border: 1px solid #c6c6c6; |
| border-radius: 1px; |
| box-sizing: border-box; |
| cursor: default; |
| position: relative; |
| display: block; |
| margin-left: auto; |
| margin-right: auto; |
| padding: 0; |
| } |
| |
| #alerts tbody tr[highlighted] td { |
| background-color: #ffffd6; |
| } |
| |
| #alerts tbody tr.group-member td:not(:first-child):not([highlighted]), |
| #alerts tbody tr[expanded]:not([highlighted]) { |
| background-color: #ebf2fc !important; |
| } |
| |
| #alerts tbody tr.group-member[highlighted] td:not(:first-child), |
| #alerts tbody tr.group-header[highlighted] { |
| background-color: #ffffd6 !important; |
| } |
| |
| #alerts tbody tr.group-member[highlighted] td:first-child { |
| background-color: transparent !important; |
| } |
| |
| /* The graph-link elements are for links to view associated graphs. */ |
| .graph-link, .graph-link:visited { |
| vertical-align: middle; |
| font-size: 1.2em; |
| color: #222; |
| } |
| |
| /* The kd-button class is used for the numbers next to rows |
| with grouped alerts. */ |
| #alerts .kd-button { |
| background-color: #f5f5f5; |
| background-image: linear-gradient(top, #f5f5f5, #f1f1f1); |
| border: 1px solid rgba(0, 0, 0, 0.1); |
| border-radius: 2px; |
| color: #444; |
| cursor: default; |
| display: block; |
| font-size: 11px; |
| font-weight: bold; |
| height: 27px; |
| line-height: 27px; |
| margin: auto; |
| min-width: 54px; |
| padding: 0 8px; |
| text-align: center; |
| transition: all 0.218s; |
| vertical-align: middle; |
| } |
| |
| #alerts .kd-button[expanded] { |
| background-color: #eee; |
| background-image: linear-gradient(top, #eee, #e0e0e0); |
| border: 1px solid #ccc; |
| box-shadow: inset 0px 1px 2px rgba(0, 0, 0, 0.1); |
| color: #333; |
| } |
| |
| #alerts .kd-button.counter { |
| height: 14px; |
| line-height: 14px; |
| min-width: 17px; |
| padding: 2px; |
| width: 17px; |
| } |
| |
| #alerts .kd-button:hover { |
| background-color: #f8f8f8; |
| background-image: linear-gradient(top, #f8f8f8, #f1f1f1); |
| border: 1px solid #c6c6c6; |
| box-shadow: 0px 1px 1px rgba(0,0,0,0.1); |
| color: #222; |
| transition: all 0.0s; |
| } |
| |
| #alerts .kd-button[hidden] { |
| display: none; |
| } |
| |
| /* Triage dialog at the top level when the user clicks the triage button. */ |
| triage-dialog { |
| position: absolute; |
| margin-top: 30px; |
| z-index: 1000; |
| } |
| </style> |
| <div> |
| <triage-dialog id="triage" on-triaged="{{onTriaged}}" xsrfToken="{{xsrfToken}}"> |
| </triage-dialog> |
| <paper-button raised id="file-bug-button" on-click="{{showTriageDialog}}"> |
| Triage |
| </paper-button> |
| <paper-button raised id="graph-button" on-click="{{showGraphs}}"> |
| Graph |
| </paper-button> |
| </div> |
| <table id="alerts"> |
| <thead> |
| <tr> |
| <th id="groupheader"></th> |
| <th id="checkheader"> |
| <input type="checkbox" id="header-checkbox" on-change="{{onHeaderCheckboxChange}}"> |
| </th> |
| <th id="graphheader"></th> |
| <th id="bug_id" on-click="{{columnHeaderClicked}}">Bug ID</th> |
| <th id="end_revision" on-click="{{columnHeaderClicked}}">Revisions</th> |
| <th id="master" on-click="{{columnHeaderClicked}}">Master</th> |
| <th id="bot" on-click="{{columnHeaderClicked}}">Bot</th> |
| <th id="testsuite" on-click="{{columnHeaderClicked}}">Test Suite</th> |
| <th id="test" on-click="{{columnHeaderClicked}}">Test</th> |
| <template repeat="{{extraColumns}}"> |
| <th id="{{key}}" on-click="{{columnHeaderClicked}}">{{label}}</th> |
| </template> |
| </tr> |
| </thead> |
| <tbody> |
| <template repeat="{{alertList}}"> |
| <tr class="{{rowType}}" |
| improvement?={{improvement}} |
| triaged?={{triaged}} |
| hidden?={{hideRow}} |
| highlighted?={{highlighted}} |
| expanded?={{expanded}} |
| on-click="{{onRowClicked}}"> |
| |
| <td> |
| <a class="kd-button counter" |
| expanded?={{expanded}} |
| on-click="{{onExpandGroupButtonClicked}}" |
| hidden?="{{!(size > 1)}}">{{size}}</a> |
| </td> |
| <td> |
| <input type="checkbox" |
| id="{{key}}" |
| checked="{{selected}}" |
| on-change="{{onCheckboxChange}}"> |
| </td> |
| |
| <td> |
| <a href="{{dashboard_link}}" class="graph-link" target="_blank"> |
| 📈 <!-- chart with upwards trend character U+1F4C8 --> |
| </a> |
| </td> |
| |
| <td hidden?="{{hide_bug_id}}"> |
| <bug-info-span bugId="{{bug_id}}" |
| key="{{key}}" |
| recovered?="{{recovered}}" |
| xsrfToken="{{xsrfToken}}" |
| on-untriaged="{{onUntriaged}}"> |
| </bug-info-span> |
| <bisect-status hidden?="{{!(bug_id > 0)}}" |
| status="{{bisect_status}}"> |
| </bisect-status> |
| </td> |
| <td class="revision_range"> |
| <revision-range start={{start_revision}} end="{{end_revision}}"></revision-range> |
| </td> |
| <td class="master"><label>{{master}}</label><label hidden?="{{(!additionColumnValues.master || expanded)}}">...</label></td> |
| <td class="bot"><label>{{bot}}</label><label hidden?="{{(!additionColumnValues.bot || expanded)}}">...</label></td> |
| <td class="testsuite"><label>{{testsuite}}</label><label hidden?="{{(!additionColumnValues.testsuite || expanded)}}">...</label></td> |
| <td class="test"><label>{{test}}</label><label hidden?="{{(!additionColumnValues.test || expanded)}}">...</label></td> |
| |
| <template repeat="{{extraColumns}}"> |
| <td class="{{key}}"><label>{{value}}</td> |
| </template> |
| </tr> |
| </template> |
| </tbody> |
| </table> |
| </template> |
| <script> |
| 'use strict'; |
| |
| (function() { |
| |
| /** |
| * Constructs a URI for the report page for this group of alerts. |
| * @param {Array.<Object>} group The group of alerts to graph. |
| * @return {string} The URI of the graph. |
| */ |
| function getGraphUri(alerts) { |
| var keys = []; |
| for (var i = 0; i < alerts.length; i++) { |
| keys.push(alerts[i]['key']); |
| } |
| return '/group_report?keys=' + keys.join(','); |
| } |
| |
| Polymer('alerts-table', { |
| |
| /** |
| * The field to sort by. Note that this will be both the id of a th |
| * element in the table, and a property of an item in the alert list. |
| */ |
| sortBy: 'end_revision', |
| |
| /** |
| * Sort direction, either 'down' (increasing) or 'up' (decreasing). |
| */ |
| sortDirection: 'down', |
| |
| /** |
| * Previous id of checkbox input element that was checked. |
| */ |
| previousCheckboxId: null, |
| |
| /** |
| * Current id of checkbox input element that was checked. |
| */ |
| currentCheckboxId: null, |
| |
| NUM_ALERTS_TO_CHECK_ON_INIT: 10, |
| |
| /** |
| * Custom element lifecycle callback, called once this element is ready. |
| */ |
| ready: function() { |
| this.checkedAlerts = []; |
| }, |
| |
| isRecursiveChange: function() { |
| if (this.isRecursingIntoChange) { |
| return true; |
| } |
| this.isRecursingIntoChange = true; |
| setTimeout(function() { |
| this.isRecursingIntoChange = false; |
| }.bind(this), 10); |
| return false; |
| }, |
| |
| /** |
| * Initializes the table. |
| * This should be called after this.alertList has been set. |
| */ |
| alertListChanged: function() { |
| // Some calls to alertListChanged can change the alert list. |
| // If that happens, don't do anything. |
| if (this.isRecursiveChange()) { |
| return; |
| } |
| this.initRowsBasedOnQueryParameters(); |
| this.showAlertsGrouped(); |
| this.updateBugColumn(); |
| if (this.extraColumns) { |
| this.prepareExtraColumnsForHeader(); |
| this.prepareExtraColumnsForBodyRows(); |
| } |
| this.maybeDisableButtons(); |
| }, |
| |
| /** |
| * Sets the columnHeaderClicked function for items in this.extraColumns. |
| * This is done so that this function is available for binding in the |
| * template above, so that the table can be sorted by any of the extra |
| * columns. |
| */ |
| prepareExtraColumnsForHeader: function() { |
| // Set the columnHeaderClicked so it can be used in in the repeat |
| // template above in the table header. |
| this.extraColumns.forEach(function(column) { |
| column.columnHeaderClicked = this.columnHeaderClicked.bind(this); |
| this.columnHeaderClicked.bind(this); |
| }, this); |
| }, |
| |
| /** |
| * Sets an extraColumns property on each of the items in this.alertList. |
| * This is done so that it can be used in in the repeat template above. |
| */ |
| prepareExtraColumnsForBodyRows: function() { |
| // Set the columnHeaderClicked so it can be used in in the repeat |
| // template above in the table header. |
| this.alertList.forEach(function(alert) { |
| alert.extraColumns = this.extraColumns.map(function(column) { |
| return {key: column.key, value: alert[column.key]}; |
| }); |
| }, this); |
| }, |
| |
| /** |
| * Displays alerts in groups. |
| */ |
| showAlertsGrouped: function() { |
| var groupMap = {}; |
| var alertOrder = []; |
| for (var i = this.alertList.length - 1; i >= 0; i--) { |
| var alert = this.alertList[i]; |
| if (alert.group) { |
| if (alert.group in groupMap) { |
| alert.rowType = 'group-member'; |
| alert.hideRow = true; |
| groupMap[alert.group].push(alert); |
| groupMap[alert.group][0].size += 1; |
| } else { |
| alert.rowType = 'group-header'; |
| alert.size = 1; |
| groupMap[alert.group] = [alert]; |
| alertOrder.push(alert.group); |
| } |
| } else { |
| alert.rowType = 'group-header'; |
| alert.size = 1; |
| groupMap[i] = [alert]; |
| alertOrder.push(i); |
| } |
| } |
| var orderedAlertList = []; |
| for (var i = alertOrder.length - 1; i >= 0; i--) { |
| orderedAlertList.push.apply( |
| orderedAlertList, groupMap[alertOrder[i]]); |
| } |
| this.alertList = orderedAlertList; |
| this.addEllipsis(); |
| }, |
| |
| /** |
| * Adds ellipsis to each column in header rows that contains different |
| * values than its group member rows. |
| */ |
| addEllipsis: function() { |
| for (var i = 0; i < this.alertList.length; i++) { |
| var alert = this.alertList[i]; |
| if (alert.rowType == 'group-header' && alert.size > 1) { |
| alert.additionColumnValues = {}; |
| for (var j = i + 1; j < this.alertList.length; j++) { |
| var memberAlert = this.alertList[j]; |
| if (memberAlert.rowType == 'group-member') { |
| if (memberAlert.master != alert.master) { |
| alert.additionColumnValues['master'] = true; |
| } |
| if (memberAlert.bot != alert.bot) { |
| alert.additionColumnValues['bot'] = true; |
| } |
| if (memberAlert.testsuite != alert.testsuite) { |
| alert.additionColumnValues['testsuite'] = true; |
| } |
| if (memberAlert.test != alert.test) { |
| alert.additionColumnValues['test'] = true; |
| } |
| } else { |
| break; |
| } |
| i++; |
| } |
| } |
| } |
| }, |
| |
| /** |
| * Toggles expansion of a group of alerts. |
| */ |
| onExpandGroupButtonClicked: function(event, detail, sender) { |
| var row = sender.parentNode.parentNode; |
| var alertIndex = row.rowIndex - 1; |
| var alert = this.alertList[alertIndex]; |
| var isExpand = !alert.expanded; |
| alert.expanded = isExpand; |
| for (var i = alertIndex + 1; i < this.alertList.length; i++) { |
| if (this.alertList[i].group == alert.group) { |
| this.alertList[i].hideRow = !isExpand; |
| } else { |
| break; |
| } |
| } |
| }, |
| |
| /** |
| * Shows, hides and checks alert rows depending on URL parameters. |
| */ |
| initRowsBasedOnQueryParameters: function() { |
| var keys = uri.getParameter('keys'); |
| if (keys != null) { |
| this.selectAlertsInKeysParameter(keys); |
| } |
| |
| // When we're looking at alerts for a particular bug, we usually want |
| // to see the graphs right away, but we also don't want to select too |
| // many alerts at once. |
| if (uri.getParameter('bug_id') && |
| this.alertList.length <= this.NUM_ALERTS_TO_CHECK_ON_INIT) { |
| this.selectFirstNAlerts(this.NUM_ALERTS_TO_CHECK_ON_INIT); |
| } |
| }, |
| |
| /** |
| * Checks the alerts enumerated in the "keys" query parameter parameter. |
| * @param {string} keys The value of the "keys" query parameter. |
| */ |
| selectAlertsInKeysParameter: function(keys) { |
| var keySet = {}; |
| keys.split(',').forEach(function(k) { |
| keySet[k] = true; |
| }); |
| for (var i = 0; i < this.alertList.length; i++) { |
| if (keySet[this.alertList[i].key]) { |
| this.alertList[i].selected = true; |
| this.alertList[i].hideRow = false; |
| } else if (this.alertList[i].improvement) { |
| this.alertList[i].hideRow = true; |
| } |
| } |
| this.onCheckboxChange(); |
| }, |
| |
| /** |
| * Selects the first |n| alerts in the table from the top. |
| */ |
| selectFirstNAlerts: function(n) { |
| for (var i = 0; i < Math.min(n, this.alertList.length); i++) { |
| this.alertList[i].selected = true; |
| this.alertList[i].hideRow = false; |
| } |
| this.onCheckboxChange(); |
| }, |
| |
| /** |
| * Shows or hides the bug id column depending on whether there are any |
| * triaged alerts listed in the table. |
| */ |
| updateBugColumn: function() { |
| // We need the xsrf token to be set in the individual bug-info-span |
| // element, and this can be done by setting it in the alert objects, |
| // and binding xsrfToken in the template above. |
| var alertsTable = this; |
| this.alertList.forEach(function(alertRow) { |
| alertRow.xsrfToken = alertsTable.xsrfToken; |
| }); |
| // Make a list of all bug IDs that indicate an alert is triaged. |
| // This includes the pseudo-bug-ids indicating invalid or ignored. |
| // Note: The 'hideRow' parameter is set in static/alerts.js, and it |
| // indicates that the 'triaged' query parameter is not set. |
| var alertsWithBugs = this.alertList.filter(function(alertRow) { |
| return alertRow.bug_id && !alertRow.hideRow; |
| }); |
| var shouldHideBugId = alertsWithBugs.length == 0; |
| // Hide the bug id th element. |
| if (shouldHideBugId) { |
| this.$.bug_id.style.display = 'none'; |
| } |
| // Hide all of the bug id data cells in the table. |
| this.alertList.forEach(function(alertRow) { |
| alertRow.hide_bug_id = shouldHideBugId; |
| }); |
| }, |
| |
| /** |
| * An event handler for the untriaged event which is fired by an |
| * alert-remove-box when the user removes a bug from an alert. |
| * @param {Event} event The event object. |
| * @param {Object} detail Parameters sent with the event. |
| * @param {Element} sender The element that sent the event. |
| */ |
| onUntriaged: function(event, detail, sender) { |
| var key = detail.key; |
| for (var i = 0; i < this.alertList.length; i++) { |
| if (this.alertList[i]['key'] == key) { |
| this.alertList[i]['bug_id'] = null; |
| } |
| } |
| }, |
| |
| /** |
| * Either unchecks or checks all alerts. |
| */ |
| onHeaderCheckboxChange: function(event, detail, sender) { |
| for (var i = 0; i < this.alertList.length; i++) { |
| var alert = this.alertList[i]; |
| if (event.target.checked) { |
| if (!alert.hideRow) { |
| alert.selected = true; |
| this.updateGroupCheckboxes(alert, i, true); |
| } |
| } else { |
| alert.selected = false; |
| } |
| } |
| this.onCheckboxChange(); |
| }, |
| |
| sortByChanged: function() { |
| this.sort(); |
| this.fire('sortby', this.sortBy); |
| }, |
| |
| sortDirectionChanged: function() { |
| this.sort(); |
| this.fire('sortdirection', this.sortDirection); |
| }, |
| |
| /** |
| * Callback for the click event for a column header. |
| * @param {Event} event Clicked event. |
| * @param {Object} detail Detail Object. |
| * @param {Element} sender Element that invoked the event. |
| */ |
| columnHeaderClicked: function(event, detail, sender) { |
| this.sortBy = sender.id; |
| var newDirection = 'down'; |
| // Because the <th> element may have been added based on an entry in |
| // this.extraColumns, this.$[this.sortBy] may not work. |
| var th = this.$.alerts.querySelector('#' + this.sortBy); |
| if (th.getAttribute('data-sort-direction') == 'down') { |
| newDirection = 'up'; |
| } |
| this.sortDirection = newDirection; |
| }, |
| |
| /** |
| * Update the table headers to indicate the current table sorting. |
| */ |
| updateHeaders: function() { |
| var headers = this.$.alerts.querySelectorAll('th'); |
| for (var i = 0; i < headers.length; i++) { |
| if (headers[i].id == this.sortBy) { |
| headers[i].setAttribute('data-sort-direction', this.sortDirection); |
| } else { |
| headers[i].removeAttribute('data-sort-direction'); |
| } |
| } |
| }, |
| |
| /** |
| * Sorts the alert list according to the current values of the properties |
| * sortDirection and sortBy. |
| */ |
| sort: function() { |
| var order = this.sortDirection == 'down' ? 1 : -1; |
| var sortBy = this.sortBy; |
| |
| // Map of group id to list of alert objects. |
| var groupMap = {}; |
| // List of alerts that should be sorted. If this is a group view, |
| // only group header alerts are added. |
| var alertsToSort = []; |
| for (var i = 0; i < this.alertList.length; i++) { |
| var alert = this.alertList[i]; |
| // Associate the current index with each element, to enable stable |
| // sorting. |
| alert.index = i; |
| |
| // Create list of group header alerts to sort by group. |
| if (alert.group) { |
| if (alert.group in groupMap) { |
| groupMap[alert.group].push(alert); |
| } else { |
| groupMap[alert.group] = [alert]; |
| alertsToSort.push(alert); |
| } |
| } else { |
| alertsToSort.push(alert); |
| } |
| } |
| |
| /** |
| * Compares two alert Objects to determine which should come first. |
| * @param {Object} alertA The first alert. |
| * @param {Object} alertB The second alert. |
| * @return {number} A negative number if alertA is first, or a |
| * positive number otherwise. |
| */ |
| var compareAlerts = function(alertA, alertB) { |
| var valA = String(alertA[sortBy]).toLowerCase(); |
| var valB = String(alertB[sortBy]).toLowerCase(); |
| |
| // If the values can be parsed as non-zero numbers, then compare |
| // numerically. Otherwise, compare lexically. |
| var parseNumber = function(str) { |
| return Number(str.match(/^\d*(\.\d+)?/)[0]); |
| }; |
| var numA = parseNumber(valA); |
| var numB = parseNumber(valB); |
| if (numA && numB) { |
| var result = numA - numB; |
| } else { |
| var result = (valA < valB) ? -1 : (valA > valB) ? 1 : 0; |
| } |
| |
| // If the alerts are equivalent on the current column, sort by their |
| // previous position. This provides a stable sort, so that users can |
| // sort by multiple columns. |
| if (result == 0) |
| result = alertA.index - alertB.index; |
| |
| return result * order; |
| }; |
| alertsToSort.sort(compareAlerts); |
| |
| var sortedAlertList = []; |
| alertsToSort.forEach(function(alert) { |
| if (alert.group in groupMap) { |
| sortedAlertList.push.apply(sortedAlertList, groupMap[alert.group]); |
| } else { |
| sortedAlertList.push(alert); |
| } |
| }); |
| this.alertList = sortedAlertList; |
| this.updateHeaders(); |
| }, |
| |
| /** |
| * Gets the intersection of the revision ranges of alerts. |
| * |
| * For example, if there were two checked alerts with the ranges |
| * [200, 400] and [300, 500], this function will return an object which |
| * represents the range [300, 400]. |
| * |
| * The input and output revision ranges are inclusive; that is, both |
| * start and end revision are included in the range. Thus the minimum |
| * revision range for alerts with ranges [110, 120] and [120, 130] is |
| * [120, 120]. |
| * |
| * @param {Array.<Object>} alerts List of alerts. |
| * @return {?Object} An object containing start and end revision, |
| * or null if the checked alerts don't overlap. |
| */ |
| getMinimumRevisionRange: function(alerts) { |
| if (!alerts || alerts.length == 0) { |
| return null; |
| } |
| // Start with the range of the first alert, and then narrow it down. |
| var start = alerts[0]['start_revision']; |
| var end = alerts[0]['end_revision']; |
| for (var i = 1; i < alerts.length; i++) { |
| var a = alerts[i]; |
| if (a['start_revision'] > start) { |
| if (a['start_revision'] > end) { |
| return null; |
| } |
| start = a['start_revision']; |
| } |
| if (a['end_revision'] < end) { |
| if (a['end_revision'] < start) { |
| return null; |
| } |
| end = a['end_revision']; |
| } |
| } |
| return {'start': start, 'end': end}; |
| }, |
| |
| /** |
| * Gets a sublist of the given list of alerts whose revision range |
| * falls within the given range. |
| * Precondition: start <= end, and for each alert in the given list, |
| * start_revision <= end_revision. |
| * @param {Array.<Object>} alerts List of alerts. |
| * @param {number} start Start revision. |
| * @param {number} end End revision. |
| * @param {Array.<Object>} Alerts that have an overlapping revision range. |
| */ |
| getOverlappingAlerts: function(alerts, start, end) { |
| return alerts.filter(function(a, index, array) { |
| return (a['start_revision'] <= end && a['end_revision'] >= start); |
| }); |
| }, |
| |
| /** |
| * Highlights alerts that overlap with checked alerts revision range. |
| */ |
| updateHighlights: function() { |
| this.alertList.forEach(function(alertRow) { |
| alertRow.highlighted = false; |
| }); |
| var minRevisionRange = this.getMinimumRevisionRange(this.checkedAlerts); |
| if (!minRevisionRange) { |
| return; |
| } |
| var overlappingAlerts = this.getOverlappingAlerts( |
| this.alertList, minRevisionRange.start, minRevisionRange.end); |
| if (overlappingAlerts.length > 1) { |
| overlappingAlerts.forEach(function(alertRow) { |
| alertRow.highlighted = true; |
| }); |
| } |
| }, |
| |
| /** |
| * Handles shift-click selecting checkboxes and selecting rows in group. |
| */ |
| onRowClicked: function(event, detail, sender) { |
| if (event.shiftKey && this.previousCheckboxId && |
| this.currentCheckboxId) { |
| if (this.previousCheckboxId == this.currentCheckboxId) { |
| return; |
| } |
| var prevIndex = 0, currentIndex = 0, isChecked = null; |
| for (var i = 0; i < this.alertList.length; i++) { |
| if (this.alertList[i].key == this.previousCheckboxId) { |
| prevIndex = i; |
| } else if (this.alertList[i].key == this.currentCheckboxId) { |
| currentIndex = i; |
| isChecked = this.alertList[i].selected; |
| } |
| } |
| // Go through and check/uncheck. |
| if (prevIndex < currentIndex) { |
| for (var i = prevIndex; i < currentIndex; i++) { |
| if (!this.alertList[i].hideRow) { |
| this.alertList[i].selected = isChecked; |
| this.updateGroupCheckboxes(this.alertList[i], i, isChecked); |
| } |
| } |
| } else { |
| for (var i = prevIndex; i > currentIndex; i--) { |
| if (!this.alertList[i].hideRow) { |
| this.alertList[i].selected = isChecked; |
| this.updateGroupCheckboxes(this.alertList[i], i, isChecked); |
| } |
| } |
| } |
| this.onCheckboxChange(); |
| } |
| }, |
| |
| /** |
| * Checks or unchecks hidden group member rows. |
| */ |
| updateGroupCheckboxes: function(alert, alertIndex, isChecked) { |
| if (alert.rowType == 'group-header' && !alert.expanded) { |
| for (var i = alertIndex + 1; i < this.alertList.length; i++) { |
| if (this.alertList[i].rowType == 'group-member') { |
| this.alertList[i].selected = isChecked; |
| } else { |
| break; |
| } |
| } |
| } |
| }, |
| |
| /** |
| * Event handler for the change event of any of the checkboxes for any |
| * alert in the table. |
| */ |
| onCheckboxChange: function(event, detail, sender) { |
| if (sender) { |
| // Checks group member rows. |
| var alertIndex = sender.parentNode.parentNode.rowIndex - 1; |
| var alert = this.alertList[alertIndex]; |
| this.updateGroupCheckboxes(alert, alertIndex, alert.selected); |
| |
| this.previousCheckboxId = this.currentCheckboxId; |
| this.currentCheckboxId = sender.id; |
| } |
| // Update the list of checked alerts. |
| this.checkedAlerts = this.alertList.filter(function(alertRow) { |
| return alertRow.selected; |
| }); |
| this.updateHeaderCheckbox(); |
| this.updateHighlights(); |
| this.maybeDisableButtons(); |
| this.fire('changeselection'); |
| }, |
| |
| /** |
| * Checks the header checkbox if all checkboxes below are checked. |
| */ |
| updateHeaderCheckbox: function() { |
| if (this.checkedAlerts.length == this.alertList.length) { |
| this.$['header-checkbox'].checked = true; |
| } else { |
| this.$['header-checkbox'].checked = false; |
| } |
| }, |
| |
| /** |
| * Disables or enables the triage and graph buttons depending on whether |
| * there are any alerts currently checked. |
| */ |
| maybeDisableButtons: function() { |
| var buttonsDisabled = this.checkedAlerts.length == 0; |
| this.$['file-bug-button'].disabled = buttonsDisabled; |
| this.$['graph-button'].disabled = buttonsDisabled; |
| }, |
| |
| /** |
| * Opens a new window on the /report page for the currently checked |
| * alerts. |
| */ |
| showGraphs: function() { |
| window.open(getGraphUri(this.checkedAlerts)); |
| }, |
| |
| /** |
| * Shows the UI to file a bug on the given group of alerts. |
| * @param {Event} e The event for the button click. |
| */ |
| showTriageDialog: function(e) { |
| this.$.triage.alerts = this.checkedAlerts; |
| this.$.triage.show(); |
| e.stopPropagation(); |
| }, |
| |
| /** |
| * Handles the 'triaged' event sent by the triage dialog; updates the UI |
| * for alerts that have been triaged. |
| * @param {Event} e The event for button click. |
| */ |
| onTriaged: function(e) { |
| var triagedKeys = e.detail.alerts.map(function(alert) { |
| return alert.key; |
| }); |
| var triagedBugId = e.detail.bugid; |
| this.checkedAlerts.forEach(function(alert) { |
| if (triagedKeys.indexOf(alert.key) != -1) { |
| alert['bug_id'] = triagedBugId; |
| if (!uri.getParameter('triaged')) { |
| alert.hideRow = true; |
| alert.selected = false; |
| } |
| } |
| }); |
| this.onCheckboxChange(); |
| this.updateBugColumn(); |
| } |
| }); |
| })(); |
| </script> |
| </polymer-element> |