blob: 781daa7a83c9078d71f6cbce4dd213be76725ec0 [file] [log] [blame]
<!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.
-->
<script src="/jquery/jquery-2.1.4.min.js"></script>
<script src="/flot/jquery.flot.min.js"></script>
<script src="/flot/jquery.flot.crosshair.min.js"></script>
<script src="/flot/jquery.flot.fillbetween.min.js"></script>
<script src="/flot/jquery.flot.selection.min.js"></script>
<link rel="import" href="/components/paper-button/paper-button.html">
<link rel="import" href="/components/polymer/polymer.html">
<link rel="import" href="/dashboard/elements/alerts-table.html">
<link rel="import" href="/dashboard/elements/bug-info.html">
<link rel="import" href="/dashboard/elements/chart-container.html">
<link rel="import" href="/dashboard/elements/login-warning.html">
<link rel="import" href="/dashboard/elements/quick-log.html">
<link rel="import" href="/dashboard/elements/triage-dialog.html">
<link rel="import" href="/dashboard/static/simple_xhr.html">
<link rel="import" href="/dashboard/static/uri.html">
<polymer-element name="group-report-page">
<template>
<style>
.error {
color: #dd4b39;
font-weight: bold;
}
/* The action bar contains the graph button and triage button. */
#action-bar {
margin-top: 20px;
width: 100%;
}
/* The top container contains the action bar and alerts list. */
#top {
display: inline-flex;
display: -webkit-inline-flex;
flex-direction: column;
-webkit-flex-direction: column;
align-items: flex-start;
-webkit-align-items: flex-start;
margin-bottom: 15px;
width: 100%
}
/* The bottom container contains the charts. */
#bottom {
display: flex;
display: -webkit-flex;
flex-direction: column;
-webkit-flex-direction: column;
min-width: 100%;
min-height: 100%;
}
/* Triage dialog at the top level when the user clicks the triage button. */
triage-dialog {
position: absolute;
margin-top: 30px;
z-index: 1000;
}
/* This class indicates a button toggled on (e.g. show improvements). */
.alert-togglebutton {
float: right;
margin-left: 4px;
margin-right: 4px;
}
#bisect-result-log {
width: 100%;
display: block;
}
#loading-spinner {
width: 100%;
display: flex;
justify-content: center;
}
</style>
<template if="{{loading}}">
<div id="loading-spinner"><img src="//www.google.com/images/loading.gif"></div>
</template>
<template if="{{error}}">
<div class="error">{{error}}</div>
</template>
<!-- TODO(sullivan): The below should be in a template if={{!loading}}.
This doesn't work correctly due to polymer 0.5 bug, let's try again
in polymer 1.0 -->
<template if="{{warningMessage}}">
<overlay-message id="warning-message" opened="true" autoCloseDisabled duration="-1">
{{warningMessage}}
<template if="{{warningBug}}">
<a href="https://github.com/catapult-project/catapult/issues/{{warningBug}}">See bug #{{warningBug}}.</a>
</template>
</overlay-message>
</template>
<login-warning id="login-warning" loginLink="{{loginUrl}}"
hidden?="{{isInternalUser}}">
</login-warning>
<div id="top">
<div id="action-bar" hidden?="{{loading || error}}">
<paper-button toggle raised
id="improvements-toggle"
class="alert-togglebutton"
on-click="{{onToggleImprovements}}">
Show all improvements
</paper-button>
</div>
<bug-info id="bug-info" xsrfToken="{{xsrfToken}}"></bug-info>
<template if="{{bugId}}">
<quick-log id="bisect-result-log"
xsrfToken="{{xsrfToken}}"
logNamespace="bisect_result"
logName="{{bugId}}"
logLabel="Bisect results"
loadOnReady="true"
expandOnReady="true"></quick-log>
</template>
<alerts-table id="alerts-table"
hidden?="{{loading || error}}"
xsrfToken="{{xsrfToken}}"
alertList="{{alertList}}"
extraColumns="{{extraColumns}}"
on-changeselection="{{onAlertSelectionChange}}"></alerts-table>
</div>
<div id="bottom">
<section id="charts-container"></section>
</div>
</template>
<script>
'use strict';
Polymer('group-report-page', {
loading: true,
get alertsTable() {
return this.$['alerts-table'];
},
getCharts() {
// Note: This cannot be a property getter for attribute charts because
// polymer caches the getter result.
var charts = [];
var children = this.$['charts-container'].children;
for (var i = 0; i < children.length; i++) {
charts.push(children[i]);
}
return charts;
},
extraColumns: [{
'key': 'percent_changed',
'label': 'Delta %'
}],
onToggleImprovements: function(event, sender, details) {
var improvementsToggle = sender;
if (improvementsToggle.hasAttribute('active')) {
this.alertsTable['alertList'].forEach(function(alert) {
if (alert['improvement']) {
alert['hideRow'] = false;
}
});
} else {
this.alertsTable['alertList'].forEach(function(alert) {
if (alert['improvement'] && !alert['selected']) {
alert['hideRow'] = true;
alert['selected'] = false;
}
});
// Make the table update its list of checked alerts.
this.alertsTable.onCheckboxChange();
}
},
alertChangedRevisions: function(event) {
var alertList = this.alertsTable['alertList'];
var nudgedAlert = event.detail['alerts'][0];
for (var i = 0; i < alertList.length; i++) {
if (alertList[i]['key'] == nudgedAlert['key']) {
alertList[i].start_revision = event.detail['startRev'];
alertList[i].end_revision = event.detail['endRev'];
// Make the table update its list of checked alerts.
this.alertsTable.onCheckboxChange();
return;
}
}
},
onGraphClose: function(event) {
// Un-check the alert in the table.
var key = event.target['alertKey'];
var alertList = this.alertsTable['alertList'];
for (var i = 0; i < alertList.length; i++) {
if (alertList[i].key == key) {
alertList[i].selected = false;
break;
}
}
// Make the table update its list of checked alerts.
// This is necessary so that the triage dialog will get a correct list
// of alerts that should be affected by a triage action.
this.alertsTable.onCheckboxChange();
// Remove the graph from the set of currently-displayed graph elements.
delete this.graphElements_[key];
},
getSubtestsEntry: function(testPath) {
var testPathParts = testPath.split('/');
var botName = testPathParts[0] + '/' + testPathParts[1];
var subtestParts = testPathParts.splice(3);
var subtestDict = this.subtests[botName][testPathParts[2]];
if (!subtestDict) {
return null;
}
for (var level = 0; level < subtestParts.length - 1; level++) {
var name = subtestParts[level];
if (!(name in subtestDict)) {
return null;
}
subtestDict = subtestDict[name]['sub_tests'];
}
return subtestDict[subtestParts[subtestParts.length - 1]];
},
getTestPath: function(alert) {
return [
alert['master'],
alert['bot'],
alert['testsuite'],
alert['test']
].join('/');
},
getTestPathAndSelectedSeries: function(alert) {
var testPath = this.getTestPath(alert);
var subtestsEntry = this.getSubtestsEntry(testPath);
var traceName = testPath.split('/').pop();
// If the "subtests" property of |subtestsEntry| is an empty object,
// that implies that this test has no subtests. In this case, show a
// chart for the parent test, with this particular child selected.
if (subtestsEntry && subtestsEntry['sub_tests'] &&
Object.keys(subtestsEntry['sub_tests']).length == 0) {
testPath = testPath.split('/').slice(0, -1).join('/');
subtestsEntry = this.getSubtestsEntry(testPath);
}
// Get a list of selected traces. This should include the series that
// the alert was on, as well as any related reference build result
// series.
var selectedTraces = [traceName];
if (subtestsEntry && subtestsEntry['sub_tests']) {
if ('ref' in subtestsEntry['sub_tests']) {
selectedTraces.push('ref');
}
if (traceName + '_ref' in subtestsEntry['sub_tests']) {
selectedTraces.push(traceName + '_ref');
}
}
// Otherwise, the test is either not found in the SUBTESTS dict, or it
// is a test with children (e.g. a summary metric). In either of these
// cases, we want to return the test path and trace found on the alert.
return [testPath, selectedTraces];
},
setChartData: function(chart) {
chart.revisionInfo = this.revisionInfo;
chart.xsrfToken = this.xsrfToken;
chart.isInternalUser = this.isInternalUser;
chart.testSuites = this.testSuites;
},
addGraph: function(alerts, insertBefore) {
if (!alerts) {
return;
}
var containerElement = this.$['charts-container'];
for (var i = 0; i < alerts.length; i++) {
var alert = alerts[i];
var chart = document.createElement('chart-container');
this.graphElements_[alert['key']] = chart;
if (insertBefore) {
containerElement.insertBefore(chart, containerElement.firstChild);
} else {
containerElement.appendChild(chart);
}
// Set graph params.
var graphParams = {
'rev': alert['end_revision']
};
chart.graphParams = graphParams;
chart.alertKey = alert['key'];
chart.addSeriesGroup([this.getTestPathAndSelectedSeries(alert)]);
chart.addEventListener('chartclosed', this.onGraphClose, false);
chart.addEventListener('alertChangedRevisions',
this.alertChangedRevisions, true);
this.setChartData(chart);
}
},
onAlertSelectionChange: function() {
// Make a set of all alerts that are checked in the table.
var alerts = {};
this.alertsTable.checkedAlerts.forEach(function(a) {
alerts[a.key] = a;
});
// Add graphs that are checked in the table but not added yet.
for (var key in alerts) {
if (!(key in this.graphElements_)) {
this.addGraph([alerts[key]], true);
}
}
// Remove graphs that are no longer checked in the table.
var chartsContainer = this.$['charts-container'];
for (var key in this.graphElements_) {
if (!(key in alerts) && key in this.graphElements_) {
if (this.graphElements_[key].parentNode == chartsContainer) {
chartsContainer.removeChild(this.graphElements_[key]);
delete this.graphElements_[key];
}
}
}
},
ready: function() {
this.graphElements_ = {};
var params = {};
var keys = uri.getParameter('keys');
if (keys) {
params['keys'] = keys;
}
var bug_id = uri.getParameter('bug_id');
if (bug_id) {
params['bug_id'] = bug_id;
}
var rev = uri.getParameter('rev');
if (rev) {
params['rev'] = rev;
}
simple_xhr.send('/group_report', params,
function(response) {
this.alertList = response['alert_list'];
this.ownerInfo = response['owner_info'];
this.subtests = response['subtests'];
this.revisionInfo = response['revision_info'];
this.loginLink = response['login_url'];
this.isInternalUser = response['is_internal_user'];
this.testSuites = response['test_suites'];
this.xsrfToken = response['xsrf_token'];
this.bugId = uri.getParameter('bug_id');
if (this.bugId) {
this.$['bug-info'].initialize(
this.bugId, this.alertsTable, this.ownerInfo);
}
this.addGraph(this.alertsTable.checkedAlerts, false);
var charts = this.getCharts();
for (var i = 0; i < charts.length; i++) {
this.setChartData(charts[i]);
}
this.loading = false;
}.bind(this),
function(msg) {
this.error = msg;
this.loading = false;
}.bind(this));
}
});
</script>
</polymer-element>