summaryrefslogtreecommitdiff
path: root/src/main/webapp
diff options
context:
space:
mode:
authorRyan Campbell <ryanjcampbell@google.com>2017-06-19 11:17:10 -0700
committerRyan Campbell <ryanjcampbell@google.com>2017-06-19 12:26:07 -0700
commit67c9a5397bd972a173cdf08aaccba44678f3aea0 (patch)
treee02d3fa1d9c91d8cfaaef276c2b3ffb4c064a4b5 /src/main/webapp
parent7054de7189ba17d6516861e437b8f50fc837191d (diff)
downloaddashboard-67c9a5397bd972a173cdf08aaccba44678f3aea0.tar.gz
Move dashboard to test/vti.
Copy code from test/vts/web to test/vti/. Bug: 62339915 Test: none Change-Id: I6e07fbaca77018f6d7bd24bd3da9e3bbbafab495
Diffstat (limited to 'src/main/webapp')
-rw-r--r--src/main/webapp/WEB-INF/appengine-web.xml31
-rw-r--r--src/main/webapp/WEB-INF/cron.xml29
-rw-r--r--src/main/webapp/WEB-INF/datastore-indexes.xml77
-rw-r--r--src/main/webapp/WEB-INF/jsp/dashboard_main.jsp177
-rw-r--r--src/main/webapp/WEB-INF/jsp/footer.jsp25
-rw-r--r--src/main/webapp/WEB-INF/jsp/header.jsp70
-rw-r--r--src/main/webapp/WEB-INF/jsp/show_coverage.jsp171
-rw-r--r--src/main/webapp/WEB-INF/jsp/show_coverage_overview.jsp168
-rw-r--r--src/main/webapp/WEB-INF/jsp/show_graph.jsp292
-rw-r--r--src/main/webapp/WEB-INF/jsp/show_performance_digest.jsp100
-rw-r--r--src/main/webapp/WEB-INF/jsp/show_plan_release.jsp109
-rw-r--r--src/main/webapp/WEB-INF/jsp/show_plan_run.jsp131
-rw-r--r--src/main/webapp/WEB-INF/jsp/show_release.jsp45
-rw-r--r--src/main/webapp/WEB-INF/jsp/show_table.jsp340
-rw-r--r--src/main/webapp/WEB-INF/jsp/show_tree.jsp225
-rw-r--r--src/main/webapp/WEB-INF/web.xml197
-rw-r--r--src/main/webapp/css/common.css25
-rw-r--r--src/main/webapp/css/dashboard_main.css109
-rw-r--r--src/main/webapp/css/datepicker.css70
-rw-r--r--src/main/webapp/css/navbar.css69
-rw-r--r--src/main/webapp/css/plan_runs.css30
-rw-r--r--src/main/webapp/css/search_header.css80
-rw-r--r--src/main/webapp/css/show_coverage.css104
-rw-r--r--src/main/webapp/css/show_graph.css70
-rw-r--r--src/main/webapp/css/show_performance_digest.css82
-rw-r--r--src/main/webapp/css/show_plan_release.css23
-rw-r--r--src/main/webapp/css/show_release.css40
-rw-r--r--src/main/webapp/css/show_table.css156
-rw-r--r--src/main/webapp/css/show_test_runs_common.css136
-rw-r--r--src/main/webapp/css/test_results.css78
-rw-r--r--src/main/webapp/js/plan_runs.js64
-rw-r--r--src/main/webapp/js/search_header.js242
-rw-r--r--src/main/webapp/js/test_results.js272
-rw-r--r--src/main/webapp/js/time.js53
34 files changed, 3890 insertions, 0 deletions
diff --git a/src/main/webapp/WEB-INF/appengine-web.xml b/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 0000000..6c7632c
--- /dev/null
+++ b/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2016 Google Inc.
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
+ <application>s~google.com:android-vts-staging</application>
+ <version>4</version>
+ <threadsafe>true</threadsafe>
+
+ <system-properties>
+ <property name="EMAIL_DOMAIN" value="${appengine.emailDomain}" />
+ <property name="SENDER_EMAIL" value="${appengine.senderEmail}" />
+ <property name="DEFAULT_EMAIL" value="${appengine.defaultEmail}" />
+ <property name="SERVICE_CLIENT_ID" value="${appengine.serviceClientID}" />
+ <property name="CLIENT_ID" value="${appengine.clientID}" />
+ <property name="GERRIT_URI" value="${gerrit.uri}" />
+ <property name="GERRIT_SCOPE" value="${gerrit.scope}" />
+ <property name="ANALYTICS_ID" value="${analytics.id}" />
+ </system-properties>
+
+</appengine-web-app> \ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/cron.xml b/src/main/webapp/WEB-INF/cron.xml
new file mode 100644
index 0000000..d8dc49e
--- /dev/null
+++ b/src/main/webapp/WEB-INF/cron.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright 2016 Google Inc. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<cronentries>
+ <cron>
+ <url>/cron/vts_alert_job</url>
+ <description>Send test failure emails.</description>
+ <schedule>every 2 minutes</schedule>
+ </cron>
+ <cron>
+ <url>/cron/vts_performance_job</url>
+ <description>Send daily performance digests.</description>
+ <schedule>every day 07:30</schedule>
+ <timezone>America/Los_Angeles</timezone>
+ </cron>
+</cronentries> \ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/datastore-indexes.xml b/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 0000000..01983f5
--- /dev/null
+++ b/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 Google Inc.
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<datastore-indexes autoGenerate="true">
+
+ <datastore-index kind="TestPlanRun" ancestor="true" source="manual">
+ <property name="type" direction="asc"/>
+ <property name="__key__" direction="desc"/>
+ </datastore-index>
+
+ <datastore-index kind="TestPlanRun" ancestor="true" source="manual">
+ <property name="type" direction="asc"/>
+ <property name="__key__" direction="asc"/>
+ </datastore-index>
+
+ <datastore-index kind="TestPlanRun" ancestor="true" source="manual">
+ <property name="__key__" direction="desc"/>
+ </datastore-index>
+
+ <datastore-index kind="Test" ancestor="false" source="manual">
+ <property name="failCount" direction="asc"/>
+ <property name="passCount" direction="asc"/>
+ </datastore-index>
+
+ <datastore-index kind="TestRun" ancestor="true" source="manual">
+ <property name="type" direction="asc"/>
+ <property name="__key__" direction="desc"/>
+ </datastore-index>
+
+ <datastore-index kind="TestRun" ancestor="true" source="manual">
+ <property name="type" direction="asc"/>
+ <property name="__key__" direction="asc"/>
+ </datastore-index>
+
+ <datastore-index kind="TestRun" ancestor="true" source="manual">
+ <property name="__key__" direction="desc"/>
+ </datastore-index>
+
+ <datastore-index kind="TestRun" ancestor="true" source="manual">
+ <property name="hasCoverage" direction="asc"/>
+ <property name="__key__" direction="desc"/>
+ </datastore-index>
+
+ <datastore-index kind="TestRun" ancestor="false" source="manual">
+ <property name="testName" direction="asc"/>
+ <property name="startTimestamp" direction="asc"/>
+ </datastore-index>
+
+ <datastore-index kind="TestRun" ancestor="false" source="manual">
+ <property name="testName" direction="asc"/>
+ <property name="startTimestamp" direction="desc"/>
+ </datastore-index>
+
+ <datastore-index kind="TestRun" ancestor="false" source="manual">
+ <property name="testName" direction="asc"/>
+ <property name="type" direction="asc"/>
+ <property name="startTimestamp" direction="asc"/>
+ </datastore-index>
+
+ <datastore-index kind="TestRun" ancestor="false" source="manual">
+ <property name="testName" direction="asc"/>
+ <property name="type" direction="asc"/>
+ <property name="startTimestamp" direction="desc"/>
+ </datastore-index>
+
+</datastore-indexes> \ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp b/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp
new file mode 100644
index 0000000..3b72110
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp
@@ -0,0 +1,177 @@
+<%--
+ ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+ ~ may not use this file except in compliance with the License. You may
+ ~ obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ ~ implied. See the License for the specific language governing
+ ~ permissions and limitations under the License.
+ --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+ <link rel='stylesheet' href='/css/dashboard_main.css'>
+ <%@ include file='header.jsp' %>
+ <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.min.js'></script>
+ <body>
+ <script>
+ var allTests = ${allTestsJson};
+ var testSet = new Set(allTests);
+ var subscriptionMap = ${subscriptionMapJson};
+
+ var addFavorite = function() {
+ if ($(this).hasClass('disabled')) {
+ return;
+ }
+ var test = $('#input-box').val();
+ if (!testSet.has(test) || test in subscriptionMap) {
+ return;
+ }
+ $('#add-button').addClass('disabled');
+ $.post('/api/favorites/' + test).then(function(data) {
+ if (!data.key) {
+ return;
+ }
+ subscriptionMap[test] = data.key;
+ var wrapper = $('<div></div>');
+ var a = $('<a></a>')
+ .attr('href', '/show_table?testName=' + test);
+ var div = $('<div class="col s11 card hoverable option"></div>');
+ div.addClass('valign-wrapper waves-effect');
+ div.appendTo(a);
+ var span = $('<span class="entry valign"></span>').text(test);
+ span.appendTo(div);
+ a.appendTo(wrapper);
+ var clear = $('<a class="col s1 btn-flat center"></a>');
+ clear.addClass('clear-button');
+ clear.append('<i class="material-icons">clear</i>');
+ clear.attr('test', test);
+ clear.appendTo(wrapper);
+ clear.click(removeFavorite);
+ wrapper.prependTo('#options').hide()
+ .slideDown(150);
+ $('#input-box').val(null);
+ Materialize.updateTextFields();
+ }).always(function() {
+ $('#add-button').removeClass('disabled');
+ });
+ }
+
+ var removeFavorite = function() {
+ var self = $(this);
+ if (self.hasClass('disabled')) {
+ return;
+ }
+ var test = self.attr('test');
+ if (!(test in subscriptionMap)) {
+ return;
+ }
+ self.addClass('disabled');
+ $.ajax({
+ url: '/api/favorites/' + subscriptionMap[test],
+ type: 'DELETE'
+ }).always(function() {
+ self.removeClass('disabled');
+ }).then(function() {
+ delete subscriptionMap[test];
+ self.parent().slideUp(150, function() {
+ self.remove();
+ });
+ });
+ }
+
+ $.widget('custom.sizedAutocomplete', $.ui.autocomplete, {
+ _resizeMenu: function() {
+ this.menu.element.outerWidth($('#input-box').width());
+ }
+ });
+
+ $(function() {
+ $('#input-box').sizedAutocomplete({
+ source: allTests,
+ classes: {
+ 'ui-autocomplete': 'card'
+ }
+ });
+
+ $('#input-box').keyup(function(event) {
+ if (event.keyCode == 13) { // return button
+ $('#add-button').click();
+ }
+ });
+
+ $('.clear-button').click(removeFavorite);
+ $('#add-button').click(addFavorite);
+ });
+ </script>
+ <div class='container'>
+ <c:choose>
+ <c:when test='${not empty error}'>
+ <div id='error-container' class='row card'>
+ <div class='col s12 center-align'>
+ <h5>${error}</h5>
+ </div>
+ </div>
+ </c:when>
+ <c:otherwise>
+ <c:set var='width' value='${showAll ? 12 : 11}' />
+ <c:if test='${not showAll}'>
+ <div class='row'>
+ <div class='input-field col s8'>
+ <input type='text' id='input-box'></input>
+ <label for='input-box'>Search for tests to add to favorites</label>
+ </div>
+ <div id='add-button-wrapper' class='col s1 valign-wrapper'>
+ <a id='add-button' class='btn-floating btn waves-effect waves-light red valign'><i class='material-icons'>add</i></a>
+ </div>
+ </div>
+ </c:if>
+ <div class='row'>
+ <div class='col s12'>
+ <h4 id='section-header'>${headerLabel}</h4>
+ </div>
+ </div>
+ <div class='row' id='options'>
+ <c:forEach items='${testNames}' var='test'>
+ <div>
+ <a href='/show_table?testName=${test.name}'>
+ <div class='col s${width} card hoverable option valign-wrapper waves-effect'>
+ <span class='entry valign'>${test.name}
+ <c:if test='${test.failCount >= 0 && test.passCount >= 0}'>
+ <c:set var='color' value='${test.failCount > 0 ? "red" : (test.passCount > 0 ? "green" : "grey")}' />
+ <span class='indicator center ${color}'>
+ ${test.passCount} / ${test.passCount + test.failCount}
+ </span>
+ </c:if>
+ </span>
+ </div>
+ </a>
+ <c:if test='${not showAll}'>
+ <a class='col s1 btn-flat center clear-button' test='${test.name}'>
+ <i class='material-icons'>clear</i>
+ </a>
+ </c:if>
+ </div>
+ </c:forEach>
+ </div>
+ </c:otherwise>
+ </c:choose>
+ </div>
+ <c:if test='${empty error}'>
+ <div class='center'>
+ <a href='${buttonLink}' id='show-button' class='btn waves-effect red'>${buttonLabel}
+ <i id='show-button-arrow' class='material-icons right'>${buttonIcon}</i>
+ </a>
+ </div>
+ </c:if>
+ <%@ include file='footer.jsp' %>
+ </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/footer.jsp b/src/main/webapp/WEB-INF/jsp/footer.jsp
new file mode 100644
index 0000000..ed2d950
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/footer.jsp
@@ -0,0 +1,25 @@
+<%--
+ ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+ ~ may not use this file except in compliance with the License. You may
+ ~ obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ ~ implied. See the License for the specific language governing
+ ~ permissions and limitations under the License.
+ --%>
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+
+<footer class='page-footer'>
+ <div class='footer-copyright'>
+ <div class='container'>
+ © 2017 - The Android Open Source Project
+ </div>
+ </div>
+</footer>
diff --git a/src/main/webapp/WEB-INF/jsp/header.jsp b/src/main/webapp/WEB-INF/jsp/header.jsp
new file mode 100644
index 0000000..77ec600
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/header.jsp
@@ -0,0 +1,70 @@
+<%--
+ ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+ ~ may not use this file except in compliance with the License. You may
+ ~ obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ ~ implied. See the License for the specific language governing
+ ~ permissions and limitations under the License.
+ --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<head>
+ <link rel='stylesheet' href='https://www.gstatic.com/external_hosted/materialize/all_styles-bundle.css'>
+ <link rel='icon' href='https://www.gstatic.com/images/branding/googleg/1x/googleg_standard_color_32dp.png' sizes='32x32'>
+ <link rel='stylesheet' href='https://fonts.googleapis.com/icon?family=Material+Icons'>
+ <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700'>
+ <link rel='stylesheet' href='/css/navbar.css'>
+ <link rel='stylesheet' href='/css/common.css'>
+ <script src='https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js'></script>
+ <script src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.min.js'></script>
+ <script src='https://www.gstatic.com/external_hosted/materialize/materialize.min.js'></script>
+ <script type='text/javascript'>
+ if (${analyticsID}) {
+ // Autogenerated from Google Analytics
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+ })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
+ ga('create', ${analyticsID}, 'auto');
+ ga('send', 'pageview');
+ }
+ </script>
+ <title>VTS Dashboard</title>
+</head>
+<body>
+ <nav id='navbar'>
+ <div class='nav-wrapper'>
+ <a href='#' class='brand-logo center'>VTS Dashboard</a>
+ <ul class='nav-list'>
+ <c:forEach items='${navbarLinks}' var='link' varStatus='loop'>
+ <li class='${loop.index == activeIndex ? "active" : ""}'>
+ <a class='nav-list-item' href='${link.url}'>${link.name}</a>
+ </li>
+ </c:forEach>
+ </ul>
+ <ul class='right'><li>
+ <a id='dropdown-button' class='dropdown-button btn red lighten-3' href='#' data-activates='dropdown'>
+ ${email}
+ </a>
+ </li></ul>
+ <ul id='dropdown' class='dropdown-content'>
+ <li><a href='${logoutURL}'>Log out</a></li>
+ </ul>
+ <c:if test='${breadcrumbLinks != null}'>
+ <div id='nav-sublist'>
+ <c:forEach items='${breadcrumbLinks}' var='link'>
+ <a href='${link.url}' class='nav-sublist-item breadcrumb'>${link.name}</a>
+ </c:forEach>
+ </div>
+ </c:if>
+ </div>
+ </nav>
+</body>
diff --git a/src/main/webapp/WEB-INF/jsp/show_coverage.jsp b/src/main/webapp/WEB-INF/jsp/show_coverage.jsp
new file mode 100644
index 0000000..790590d
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_coverage.jsp
@@ -0,0 +1,171 @@
+<%--
+ ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+ ~ may not use this file except in compliance with the License. You may
+ ~ obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ ~ implied. See the License for the specific language governing
+ ~ permissions and limitations under the License.
+ --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+ <%@ include file="header.jsp" %>
+ <link rel="stylesheet" href="/css/show_coverage.css">
+ <script src="https://apis.google.com/js/api.js" type="text/javascript"></script>
+ <body>
+ <script type="text/javascript">
+ var coverageVectors = ${coverageVectors};
+ $(document).ready(function() {
+ // Initialize AJAX for CORS
+ $.ajaxSetup({
+ xhrFields : {
+ withCredentials: true
+ }
+ });
+
+ // Initialize auth2 client and scope for requests to Gerrit
+ gapi.load('auth2', function() {
+ var auth2 = gapi.auth2.init({
+ client_id: ${clientId},
+ scope: ${gerritScope}
+ });
+ auth2.then(displayEntries);
+ });
+ });
+
+ /* Open a window to Gerrit so that user can login.
+ Minimize the previously clicked entry.
+ */
+ var gerritLogin = function(element) {
+ window.open(${gerritURI}, "Ratting", "toolbar=0,status=0");
+ element.click();
+ }
+
+ /* Loads source code for a particular entry and displays it with
+ coverage information as the accordion entry expands.
+ */
+ var onClick = function() {
+ // Remove source code from the accordion entry that was open before
+ var self = $(this);
+ var prev = self.parent().siblings('li.active');
+ if (prev.length > 0) {
+ prev.find('.table-container').empty();
+ }
+ var url = self.parent().attr('url');
+ var i = self.parent().attr('index');
+ var container = self.parent().find('.table-container');
+ container.html('<div class="center-align">Loading...</div>');
+ if (self.parent().hasClass('active')) {
+ // Remove the code from display
+ container.empty();
+ } else {
+ /* Fetch and display the code.
+ Note: a coverageVector may be shorter than sourceContents due
+ to non-executable (i.e. comments or language-specific syntax)
+ lines in the code. Trailing source lines that have no
+ coverage information are assumed to be non-executable.
+ */
+ $.ajax({
+ url: url,
+ dataType: 'text'
+ }).promise().done(function(src) {
+ src = atob(src);
+ if (!src) return;
+ srcLines = src.split('\n');
+ covered = 0;
+ total = 0;
+ var table = $('<table class="table"></table>');
+ var rows = srcLines.forEach(function(line, j) {
+ var count = coverageVectors[i][j];
+ var row = $('<tr></tr>');
+ if (typeof count == 'undefined' || count < 0) {
+ count = "--";
+ } else if (count == 0) {
+ row.addClass('uncovered');
+ total += 1;
+ } else {
+ row.addClass('covered');
+ total += 1;
+ }
+ row.append('<td class="count">' + String(count) + '</td>');
+ row.append('<td class="line_no">' + String(j+1) + '</td>');
+ code = $('<td class="code"></td>');
+ code.text(String(line));
+ code.appendTo(row);
+ row.appendTo(table);
+ });
+ container.empty();
+ container.append(table);
+ }).fail(function(error) {
+ if (error.status == 0) { // origin error, refresh cookie
+ container.empty();
+ container.html('<div class="center-align">' +
+ '<span class="login-button">' +
+ 'Click to authorize Gerrit access' +
+ '</span></div>');
+ container.find('.login-button').click(function() {
+ gerritLogin(self);
+ });
+ } else {
+ container.html('<div class="center-align">' +
+ 'Not found.</div>');
+ }
+ });
+ }
+ }
+
+ /* Appends a row to the display with test name and aggregated coverage
+ information. On expansion, source code is loaded with coverage
+ highlighted by calling 'onClick'.
+ */
+ var displayEntries = function() {
+ var sourceFilenames = ${sourceFiles};
+ var sectionMap = ${sectionMap};
+ var gerritURI = ${gerritURI};
+ var projects = ${projects};
+ var commits = ${commits};
+ var indicators = ${indicators};
+ Object.keys(sectionMap).forEach(function(section) {
+ var indices = sectionMap[section];
+ var html = String();
+ indices.forEach(function(i) {
+ var url = gerritURI + '/projects/' +
+ encodeURIComponent(projects[i]) + '/commits/' +
+ encodeURIComponent(commits[i]) + '/files/' +
+ encodeURIComponent(sourceFilenames[i]) +
+ '/content';
+ html += '<li url="' + url + '" index="' + i + '">' +
+ '<div class="collapsible-header">' +
+ '<i class="material-icons">library_books</i>' +
+ sourceFilenames[i] + indicators[i] + '</div>';
+ html += '<div class="collapsible-body row">' +
+ '<div class="html-container">' +
+ '<div class="table-container"></div>' +
+ '</div></div></li>';
+ });
+ if (html) {
+ html = '<h4 class="section-title"><b>Coverage:</b> ' +
+ section + '</h4><ul class="collapsible popout" ' +
+ 'data-collapsible="accordion">' + html + '</ul>';
+ $('#coverage-container').append(html);
+ }
+ });
+ $('.collapsible.popout').collapsible({
+ accordion : true
+ }).find('.collapsible-header').click(onClick);
+ }
+ </script>
+ <div id='coverage-container' class='wide container'>
+ </div>
+ <%@ include file="footer.jsp" %>
+ </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_coverage_overview.jsp b/src/main/webapp/WEB-INF/jsp/show_coverage_overview.jsp
new file mode 100644
index 0000000..b06e9df
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_coverage_overview.jsp
@@ -0,0 +1,168 @@
+<%--
+ ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+ ~ may not use this file except in compliance with the License. You may
+ ~ obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ ~ implied. See the License for the specific language governing
+ ~ permissions and limitations under the License.
+ --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+ <!-- <link rel='stylesheet' href='/css/dashboard_main.css'> -->
+ <%@ include file='header.jsp' %>
+ <link type='text/css' href='/css/show_test_runs_common.css' rel='stylesheet'>
+ <link type='text/css' href='/css/test_results.css' rel='stylesheet'>
+ <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
+ <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+ <script src='js/time.js'></script>
+ <script src='js/test_results.js'></script>
+ <script type='text/javascript'>
+ google.charts.load('current', {'packages':['table', 'corechart']});
+ google.charts.setOnLoadCallback(drawStatsChart);
+ google.charts.setOnLoadCallback(drawCoverageCharts);
+
+ $(document).ready(function() {
+ $('#test-results-container').showTests(${testRuns}, true);
+ });
+
+ // draw test statistics chart
+ function drawStatsChart() {
+ var testStats = ${testStats};
+ if (testStats.length < 1) {
+ return;
+ }
+ var resultNames = ${resultNamesJson};
+ var rows = resultNames.map(function(res, i) {
+ nickname = res.replace('TEST_CASE_RESULT_', '').replace('_', ' ')
+ .trim().toLowerCase();
+ return [nickname, parseInt(testStats[i])];
+ });
+ rows.unshift(['Result', 'Count']);
+
+ // Get CSS color definitions (or default to white)
+ var colors = resultNames.map(function(res) {
+ return $('.' + res).css('background-color') || 'white';
+ });
+
+ var data = google.visualization.arrayToDataTable(rows);
+ var options = {
+ is3D: false,
+ colors: colors,
+ fontName: 'Roboto',
+ fontSize: '14px',
+ legend: {position: 'labeled'},
+ tooltip: {showColorCode: true, ignoreBounds: false},
+ chartArea: {height: '80%', width: '90%'},
+ pieHole: 0.4
+ };
+
+ var chart = new google.visualization.PieChart(document.getElementById('pie-chart-stats'));
+ chart.draw(data, options);
+ }
+
+ // draw the coverage pie charts
+ function drawCoverageCharts() {
+ var coveredLines = ${coveredLines};
+ var uncoveredLines = ${uncoveredLines};
+ var rows = [
+ ["Result", "Count"],
+ ["Covered Lines", coveredLines],
+ ["Uncovered Lines", uncoveredLines]
+ ];
+
+ // Get CSS color definitions (or default to white)
+ var colors = [
+ $('.TEST_CASE_RESULT_PASS').css('background-color') || 'white',
+ $('.TEST_CASE_RESULT_FAIL').css('background-color') || 'white'
+ ]
+
+ var data = google.visualization.arrayToDataTable(rows);
+
+
+ var optionsRaw = {
+ is3D: false,
+ colors: colors,
+ fontName: 'Roboto',
+ fontSize: '14px',
+ pieSliceText: 'value',
+ legend: {position: 'bottom'},
+ chartArea: {height: '80%', width: '90%'},
+ tooltip: {showColorCode: true, ignoreBounds: false, text: 'value'},
+ pieHole: 0.4
+ };
+
+ var optionsNormalized = {
+ is3D: false,
+ colors: colors,
+ fontName: 'Roboto',
+ fontSize: '14px',
+ legend: {position: 'bottom'},
+ tooltip: {showColorCode: true, ignoreBounds: false, text: 'percentage'},
+ chartArea: {height: '80%', width: '90%'},
+ pieHole: 0.4
+ };
+
+ var chart = new google.visualization.PieChart(document.getElementById('pie-chart-coverage-raw'));
+ chart.draw(data, optionsRaw);
+
+ chart = new google.visualization.PieChart(document.getElementById('pie-chart-coverage-normalized'));
+ chart.draw(data, optionsNormalized);
+ }
+
+ </script>
+
+ <body>
+ <div class='wide container'>
+ <div class='row'>
+ <div class='col s12'>
+ <div class='col s12 card center-align'>
+ <div id='legend-wrapper'>
+ <c:forEach items='${resultNames}' var='res'>
+ <div class='center-align legend-entry'>
+ <c:set var='trimmed' value='${fn:replace(res, "TEST_CASE_RESULT_", "")}'/>
+ <c:set var='nickname' value='${fn:replace(trimmed, "_", " ")}'/>
+ <label for='${res}'>${nickname}</label>
+ <div id='${res}' class='${res} legend-bubble'></div>
+ </div>
+ </c:forEach>
+ </div>
+ </div>
+ </div>
+ <div class='col s4 valign-wrapper'>
+ <!-- pie chart -->
+ <div class='pie-chart-wrapper col s12 valign center-align card'>
+ <h6 class='pie-chart-title'>Test Statistics</h6>
+ <div id='pie-chart-stats' class='pie-chart-div'></div>
+ </div>
+ </div>
+ <div class='col s4 valign-wrapper'>
+ <!-- pie chart -->
+ <div class='pie-chart-wrapper col s12 valign center-align card'>
+ <h6 class='pie-chart-title'>Coverage (Raw)</h6>
+ <div id='pie-chart-coverage-raw' class='pie-chart-div'></div>
+ </div>
+ </div>
+ <div class='col s4 valign-wrapper'>
+ <!-- pie chart -->
+ <div class='pie-chart-wrapper col s12 valign center-align card'>
+ <h6 class='pie-chart-title'>Coverage (Normalized)</h6>
+ <div id='pie-chart-coverage-normalized' class='pie-chart-div'></div>
+ </div>
+ </div>
+ </div>
+ <div class='col s12' id='test-results-container'>
+ </div>
+ </div>
+ <%@ include file="footer.jsp" %>
+ </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_graph.jsp b/src/main/webapp/WEB-INF/jsp/show_graph.jsp
new file mode 100644
index 0000000..7111f58
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_graph.jsp
@@ -0,0 +1,292 @@
+<%--
+ ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+ ~ may not use this file except in compliance with the License. You may
+ ~ obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ ~ implied. See the License for the specific language governing
+ ~ permissions and limitations under the License.
+ --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+ <%@ include file="header.jsp" %>
+ <link type='text/css' href='/css/datepicker.css' rel='stylesheet'>
+ <link type='text/css' href='/css/show_graph.css' rel='stylesheet'>
+ <link rel='stylesheet' href='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.css'>
+ <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
+ <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js'></script>
+ <body>
+ <script type='text/javascript'>
+ google.charts.load('current', {packages:['corechart', 'table', 'line']});
+ google.charts.setOnLoadCallback(drawAllGraphs);
+
+ ONE_DAY = 86400000000;
+ MICRO_PER_MILLI = 1000;
+ N_BUCKETS = 200;
+
+ var graphs = ${graphs};
+
+ $(function() {
+ $('select').material_select();
+ var date = $('#date').datepicker({
+ showAnim: 'slideDown',
+ maxDate: new Date()
+ });
+ date.datepicker('setDate', new Date(${endTime} / MICRO_PER_MILLI));
+ $('#load').click(load);
+ $('#outlier-select').change(drawAllGraphs);
+ });
+
+ // Draw all graphs.
+ function drawAllGraphs() {
+ $('#profiling-container').empty();
+ var percentileIndex = Number($('#outlier-select').val());
+
+ // Get histogram extrema
+ var histMin = null;
+ var histMax = null;
+ graphs.forEach(function(g) {
+ if (g.type != 'HISTOGRAM') return;
+ var minVal;
+ var maxVal;
+ if (percentileIndex == -1) {
+ minVal = g.min;
+ maxVal = g.max;
+ } else {
+ minVal = g.percentile_values[percentileIndex];
+ var endIndex = g.percentiles.length - percentileIndex - 1
+ maxVal = g.percentile_values[endIndex];
+ }
+ if (!histMin || minVal < histMin) histMin = minVal;
+ if (!histMax || maxVal > histMax) histMax = maxVal;
+ });
+
+ graphs.forEach(function(graph) {
+ if (graph.type == 'LINE_GRAPH') drawLineGraph(graph);
+ else if (graph.type == 'HISTOGRAM')
+ drawHistogram(graph, histMin, histMax);
+ });
+ }
+
+ /**
+ * Draw a line graph.
+ *
+ * Args:
+ * lineGraph: a JSON object containing the following fields:
+ * - name: the name of the graph
+ * - values: an array of numbers
+ * - ticks: an array of strings to use as x-axis labels
+ * - ids: an array of string labels for each point (e.g. the
+ * build info for the run that produced the point)
+ * - x_label: the string label for the x axis
+ * - y_label: the string label for the y axis
+ */
+ function drawLineGraph(lineGraph) {
+ if (!lineGraph.ticks || lineGraph.ticks.length < 1) {
+ return;
+ }
+ var title = 'Performance';
+ if (lineGraph.name) title += ' (' + lineGraph.name + ')';
+ lineGraph.ticks.forEach(function (label, i) {
+ lineGraph.values[i].unshift(label);
+ });
+ var data = new google.visualization.DataTable();
+ data.addColumn('string', lineGraph.x_label);
+ lineGraph.ids.forEach(function(id) {
+ data.addColumn('number', id);
+ });
+ data.addRows(lineGraph.values);
+ var options = {
+ chart: {
+ title: title,
+ subtitle: lineGraph.y_label
+ },
+ legend: { position: 'none' }
+ };
+ var container = $('<div class="row card center-align col s12 graph-wrapper"></div>');
+ container.appendTo('#profiling-container');
+ var chartDiv = $('<div class="col s12 graph"></div>');
+ chartDiv.appendTo(container);
+ var chart = new google.charts.Line(chartDiv[0]);
+ chart.draw(data, options);
+ }
+
+ /**
+ * Draw a histogram.
+ *
+ * Args:
+ * hist: a JSON object containing the following fields:
+ * - name: the name of the graph
+ * - values: an array of numbers
+ * - ids: an array of string labels for each point (e.g. the
+ * build info for the run that produced the point)
+ * - x_label: the string label for the x axis
+ * - y_label: the string label for the y axis
+ * min: the minimum value to display
+ * max: the maximum value to display
+ */
+ function drawHistogram(hist, min, max) {
+ if (!hist.values || hist.values.length == 0) return;
+ var title = 'Performance';
+ if (hist.name) title += ' (' + hist.name + ')';
+ var values = hist.values;
+ var histogramData = values.reduce(function(result, d, i) {
+ if (d <= max && d >= min) result.push([hist.ids[i], d]);
+ return result;
+ }, []);
+
+ var data = google.visualization.arrayToDataTable(histogramData, true);
+ var bucketSize = (max - min) / N_BUCKETS;
+
+ var options = {
+ title: title,
+ titleTextStyle: {
+ color: '#757575',
+ fontSize: 16,
+ bold: false
+ },
+ legend: { position: 'none' },
+ colors: ['#4285F4'],
+ fontName: 'Roboto',
+ vAxis:{
+ title: hist.y_label,
+ titleTextStyle: {
+ color: '#424242',
+ fontSize: 12,
+ italic: false
+ },
+ textStyle: {
+ fontSize: 12,
+ color: '#757575'
+ },
+ },
+ hAxis: {
+ title: hist.x_label,
+ textStyle: {
+ fontSize: 12,
+ color: '#757575'
+ },
+ titleTextStyle: {
+ color: '#424242',
+ fontSize: 12,
+ italic: false
+ }
+ },
+ bar: { gap: 0 },
+ histogram: {
+ minValue: min,
+ maxValue: max,
+ maxNumBuckets: N_BUCKETS,
+ bucketSize: bucketSize
+ },
+ chartArea: {
+ width: '100%',
+ top: 40,
+ left: 60,
+ height: 375
+ }
+ };
+ var container = $('<div class="row card col s12 graph-wrapper"></div>');
+ container.appendTo('#profiling-container');
+
+ var chartDiv = $('<div class="col s12 graph"></div>');
+ chartDiv.appendTo(container);
+ var chart = new google.visualization.Histogram(chartDiv[0]);
+ chart.draw(data, options);
+
+ var tableDiv = $('<div class="col s12"></div>');
+ tableDiv.appendTo(container);
+
+ var tableHtml = '<table class="percentile-table"><thead><tr>';
+ hist.percentiles.forEach(function(p) {
+ tableHtml += '<th data-field="id">' + p + '%</th>';
+ });
+ tableHtml += '</tr></thead><tbody><tr>';
+ hist.percentile_values.forEach(function(v) {
+ tableHtml += '<td>' + v + '</td>';
+ });
+ tableHtml += '</tbody></table>';
+ $(tableHtml).appendTo(tableDiv);
+ }
+
+ // Reload the page.
+ function load() {
+ var endTime = $('#date').datepicker('getDate').getTime();
+ endTime = endTime + (ONE_DAY / MICRO_PER_MILLI) - 1;
+ var filterVal = $('#outlier-select').val();
+ var ctx = '${pageContext.request.contextPath}';
+ var link = ctx + '/show_graph?profilingPoint=${profilingPointName}' +
+ '&testName=${testName}' +
+ '&endTime=' + (endTime * MICRO_PER_MILLI) +
+ '&filterVal=' + filterVal;
+ if ($('#device-select').prop('selectedIndex') > 1) {
+ link += '&device=' + $('#device-select').val();
+ }
+ window.open(link,'_self');
+ }
+ </script>
+ <div id='download' class='fixed-action-btn'>
+ <a id='b' class='btn-floating btn-large red waves-effect waves-light'>
+ <i class='large material-icons'>file_download</i>
+ </a>
+ </div>
+ <div class='container wide'>
+ <div class='row card'>
+ <div id='header-container' class='valign-wrapper col s12'>
+ <div class='col s3 valign'>
+ <h5>Profiling Point:</h5>
+ </div>
+ <div class='col s9 right-align valign'>
+ <h5 class='profiling-name truncate'>${profilingPointName}</h5>
+ </div>
+ </div>
+ <div id='date-container' class='col s12'>
+ <c:set var='offset' value='${showFilterDropdown ? 0 : 2}' />
+ <c:if test='${showFilterDropdown}'>
+ <div id='outlier-select-wrapper' class='col s2'>
+ <select id='outlier-select'>
+ <option value='-1' ${filterVal eq -1 ? 'selected' : ''}>Show outliers</option>
+ <option value='0' ${filterVal eq 0 ? 'selected' : ''}>Filter outliers (1%)</option>
+ <option value='1' ${filterVal eq 1 ? 'selected' : ''}>Filter outliers (2%)</option>
+ <option value='2' ${filterVal eq 2 ? 'selected' : ''}>Filter outliers (5%)</option>
+ </select>
+ </div>
+ </c:if>
+ <div id='device-select-wrapper' class='input-field col s5 m3 offset-m${offset + 4} offset-s${offset}'>
+ <select id='device-select'>
+ <option value='' disabled>Select device</option>
+ <option value='0' ${empty selectedDevice ? 'selected' : ''}>All Devices</option>
+ <c:forEach items='${devices}' var='device' varStatus='loop'>
+ <option value=${device} ${selectedDevice eq device ? 'selected' : ''}>${device}</option>
+ </c:forEach>
+ </select>
+ </div>
+ <input type='text' id='date' name='date' class='col s4 m2'>
+ <a id='load' class='btn-floating btn-medium red right waves-effect waves-light'>
+ <i class='medium material-icons'>cached</i>
+ </a>
+ </div>
+ </div>
+ <div id='profiling-container'>
+ </div>
+ <c:if test='${not empty error}'>
+ <div id='error-container' class='row card'>
+ <div class='col s10 offset-s1 center-align'>
+ <!-- Error in case of profiling data is missing -->
+ <h5>${error}</h5>
+ </div>
+ </div>
+ </c:if>
+ </div>
+ <%@ include file="footer.jsp" %>
+ </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_performance_digest.jsp b/src/main/webapp/WEB-INF/jsp/show_performance_digest.jsp
new file mode 100644
index 0000000..224d847
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_performance_digest.jsp
@@ -0,0 +1,100 @@
+<%--
+ ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+ ~ may not use this file except in compliance with the License. You may
+ ~ obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ ~ implied. See the License for the specific language governing
+ ~ permissions and limitations under the License.
+ --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+ <%@ include file="header.jsp" %>
+ <link type='text/css' href='/css/datepicker.css' rel='stylesheet'>
+ <link type='text/css' href='/css/show_performance_digest.css' rel='stylesheet'>
+ <link rel='stylesheet' href='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.css'>
+ <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+ <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
+ <body>
+ <script type='text/javascript'>
+ ONE_DAY = 86400000000;
+ MICRO_PER_MILLI = 1000;
+
+ function load() {
+ var time = $('#date').datepicker('getDate').getTime() - 1;
+ time = time * MICRO_PER_MILLI + ONE_DAY; // end of day
+ var ctx = '${pageContext.request.contextPath}';
+ var link = ctx + '/show_performance_digest?profilingPoint=${profilingPointName}' +
+ '&testName=${testName}' +
+ '&startTime=' + time;
+ if ($('#device-select').prop('selectedIndex') > 1) {
+ link += '&device=' + $('#device-select').val();
+ }
+ window.open(link,'_self');
+ }
+
+ $(function() {
+ var date = $('#date').datepicker({
+ showAnim: "slideDown",
+ maxDate: new Date()
+ });
+ date.datepicker('setDate', new Date(${startTime} / MICRO_PER_MILLI));
+ $('#load').click(load);
+
+ $('.date-label').each(function(i) {
+ var label = $(this);
+ label.html(moment(parseInt(label.html())).format('M/D/YY'));
+ });
+ $('select').material_select();
+ });
+ </script>
+ <div class='wide container'>
+ <div class='row card'>
+ <div id='header-container' class='col s12'>
+ <div class='col s12'>
+ <h4>Daily Performance Digest</h4>
+ </div>
+ <div id='device-select-wrapper' class='input-field col s6 m3 offset-m6'>
+ <select id='device-select'>
+ <option value='' disabled>Select device</option>
+ <option value='0' ${empty selectedDevice ? 'selected' : ''}>All Devices</option>
+ <c:forEach items='${devices}' var='device' varStatus='loop'>
+ <option value=${device} ${selectedDevice eq device ? 'selected' : ''}>${device}</option>
+ </c:forEach>
+ </select>
+ </div>
+ <input type='text' id='date' name='date' class='col s5 m2'>
+ <a id='load' class='btn-floating btn-medium red right waves-effect waves-light'>
+ <i class='medium material-icons'>cached</i>
+ </a>
+ </div>
+ </div>
+ <div class='row'>
+ <c:forEach items='${tables}' var='table' varStatus='loop'>
+ <div class='col s12 card summary'>
+ <div class='col s3 valign'>
+ <h5>Profiling Point:</h5>
+ </div>
+ <div class='col s9 right-align valign'>
+ <h5 class="profiling-name truncate">${tableTitles[loop.index]}</h5>
+ </div>
+ ${table}
+ <span class='profiling-subtitle'>
+ ${tableSubtitles[loop.index]}
+ </span>
+ </div>
+ </c:forEach>
+ </div>
+ </div>
+ <%@ include file="footer.jsp" %>
+ </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_plan_release.jsp b/src/main/webapp/WEB-INF/jsp/show_plan_release.jsp
new file mode 100644
index 0000000..b202d04
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_plan_release.jsp
@@ -0,0 +1,109 @@
+<%--
+ ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+ ~ may not use this file except in compliance with the License. You may
+ ~ obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ ~ implied. See the License for the specific language governing
+ ~ permissions and limitations under the License.
+ --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+ <%@ include file="header.jsp" %>
+ <link rel='stylesheet' href='/css/show_plan_release.css'>
+ <link rel='stylesheet' href='/css/plan_runs.css'>
+ <link rel='stylesheet' href='/css/search_header.css'>
+ <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+ <script src='js/time.js'></script>
+ <script src='js/plan_runs.js'></script>
+ <script src='js/search_header.js'></script>
+ <script type='text/javascript'>
+ var search;
+ $(document).ready(function() {
+ // disable buttons on load
+ if (!${hasNewer}) {
+ $('#newer-button').toggleClass('disabled');
+ }
+ if (!${hasOlder}) {
+ $('#older-button').toggleClass('disabled');
+ }
+
+ $('#newer-button').click(prev);
+ $('#older-button').click(next);
+ search = $('#filter-bar').createSearchHeader('Plan: ', '${plan}', refresh);
+ search.addFilter('Branch', 'branch', {
+ corpus: ${branches}
+ }, ${branch});
+ search.addFilter('Device', 'device', {
+ corpus: ${devices}
+ }, ${device});
+ search.addFilter('Device Build ID', 'deviceBuildId', {}, ${deviceBuildId});
+ search.addRunTypeCheckboxes(${showPresubmit}, ${showPostsubmit});
+ search.display();
+ $('#release-container').showPlanRuns(${planRuns});
+ });
+
+ // view older data
+ function next() {
+ if($(this).hasClass('disabled')) return;
+ var endTime = ${startTime};
+ var link = '${pageContext.request.contextPath}' +
+ '/show_plan_release?plan=${plan}&endTime=' + endTime +
+ search.args();
+ if (${unfiltered}) {
+ link += '&unfiltered=';
+ }
+ window.open(link,'_self');
+ }
+
+ // view newer data
+ function prev() {
+ if($(this).hasClass('disabled')) return;
+ var startTime = ${endTime};
+ var link = '${pageContext.request.contextPath}' +
+ '/show_plan_release?plan=${plan}&startTime=' + startTime +
+ search.args();
+ if (${unfiltered}) {
+ link += '&unfiltered=';
+ }
+ window.open(link,'_self');
+ }
+
+ // refresh the page to see the runs matching the specified filter
+ function refresh() {
+ var link = '${pageContext.request.contextPath}' +
+ '/show_plan_release?plan=${plan}' + search.args();
+ if (${unfiltered}) {
+ link += '&unfiltered=';
+ }
+ window.open(link,'_self');
+ }
+ </script>
+
+ <body>
+ <div class='wide container'>
+ <div id='filter-bar'></div>
+ <div class='row' id='release-container'></div>
+ <div id='newer-wrapper' class='page-button-wrapper fixed-action-btn'>
+ <a id='newer-button' class='btn-floating btn red waves-effect'>
+ <i class='large material-icons'>keyboard_arrow_left</i>
+ </a>
+ </div>
+ <div id='older-wrapper' class='page-button-wrapper fixed-action-btn'>
+ <a id='older-button' class='btn-floating btn red waves-effect'>
+ <i class='large material-icons'>keyboard_arrow_right</i>
+ </a>
+ </div>
+ </div>
+ <%@ include file="footer.jsp" %>
+ </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_plan_run.jsp b/src/main/webapp/WEB-INF/jsp/show_plan_run.jsp
new file mode 100644
index 0000000..491703a
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_plan_run.jsp
@@ -0,0 +1,131 @@
+<%--
+ ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+ ~ may not use this file except in compliance with the License. You may
+ ~ obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ ~ implied. See the License for the specific language governing
+ ~ permissions and limitations under the License.
+ --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+ <%@ include file="header.jsp" %>
+ <link type='text/css' href='/css/show_test_runs_common.css' rel='stylesheet'>
+ <link type='text/css' href='/css/test_results.css' rel='stylesheet'>
+ <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
+ <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+ <script src='js/time.js'></script>
+ <script src='js/test_results.js'></script>
+ <script type='text/javascript'>
+ google.charts.load('current', {'packages':['table', 'corechart']});
+ google.charts.setOnLoadCallback(drawPieChart);
+
+ $(document).ready(function() {
+ $('#test-results-container').showTests(${testRuns}, true);
+ });
+
+ // to draw pie chart
+ function drawPieChart() {
+ var topBuildResultCounts = ${topBuildResultCounts};
+ if (topBuildResultCounts.length < 1) {
+ return;
+ }
+ var resultNames = ${resultNamesJson};
+ var rows = resultNames.map(function(res, i) {
+ nickname = res.replace('TEST_CASE_RESULT_', '').replace('_', ' ')
+ .trim().toLowerCase();
+ return [nickname, parseInt(topBuildResultCounts[i])];
+ });
+ rows.unshift(['Result', 'Count']);
+
+ // Get CSS color definitions (or default to white)
+ var colors = resultNames.map(function(res) {
+ return $('.' + res).css('background-color') || 'white';
+ });
+
+ var data = google.visualization.arrayToDataTable(rows);
+ var options = {
+ is3D: false,
+ colors: colors,
+ fontName: 'Roboto',
+ fontSize: '14px',
+ legend: 'none',
+ tooltip: {showColorCode: true, ignoreBounds: true},
+ chartArea: {height: '90%'}
+ };
+
+ var chart = new google.visualization.PieChart(document.getElementById('pie-chart-div'));
+ chart.draw(data, options);
+ }
+ </script>
+
+ <body>
+ <div class='wide container'>
+ <div class='row'>
+ <div class='col s7'>
+ <div class='col s12 card center-align'>
+ <div id='legend-wrapper'>
+ <c:forEach items='${resultNames}' var='res'>
+ <div class='center-align legend-entry'>
+ <c:set var='trimmed' value='${fn:replace(res, "TEST_CASE_RESULT_", "")}'/>
+ <c:set var='nickname' value='${fn:replace(trimmed, "_", " ")}'/>
+ <label for='${res}'>${nickname}</label>
+ <div id='${res}' class='${res} legend-bubble'></div>
+ </div>
+ </c:forEach>
+ </div>
+ </div>
+ <div id='profiling-container' class='col s12'>
+ <c:choose>
+ <c:when test='${empty profilingPointNames}'>
+ <div id='error-div' class='center-align card'><h5>${error}</h5></div>
+ </c:when>
+ <c:otherwise>
+ <ul id='profiling-body' class='collapsible' data-collapsible='accordion'>
+ <li>
+ <div class='collapsible-header'><i class='material-icons'>timeline</i>Profiling Graphs</div>
+ <div class='collapsible-body'>
+ <ul id='profiling-list' class='collection'>
+ <c:forEach items='${profilingPointNames}' var='pt'>
+ <c:set var='profPointArgs' value='testName=${testName}&profilingPoint=${pt}'/>
+ <c:set var='timeArgs' value='endTime=${endTime}'/>
+ <a href='/show_graph?${profPointArgs}&${timeArgs}'
+ class='collection-item profiling-point-name'>${pt}
+ </a>
+ </c:forEach>
+ </ul>
+ </div>
+ </li>
+ <li>
+ <a class='collapsible-link' href='/show_performance_digest?testName=${testName}'>
+ <div class='collapsible-header'><i class='material-icons'>toc</i>Performance Digest</div>
+ </a>
+ </li>
+ </ul>
+ </c:otherwise>
+ </c:choose>
+ </div>
+ </div>
+ <div class='col s5 valign-wrapper'>
+ <!-- pie chart -->
+ <div id='pie-chart-wrapper' class='col s12 valign center-align card'>
+ <div id='pie-chart-div'></div>
+ </div>
+ </div>
+ </div>
+
+ <div class='col s12' id='test-results-container'>
+ </div>
+ </div>
+ <%@ include file="footer.jsp" %>
+ </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_release.jsp b/src/main/webapp/WEB-INF/jsp/show_release.jsp
new file mode 100644
index 0000000..b3da353
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_release.jsp
@@ -0,0 +1,45 @@
+<%--
+ ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+ ~ may not use this file except in compliance with the License. You may
+ ~ obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ ~ implied. See the License for the specific language governing
+ ~ permissions and limitations under the License.
+ --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+ <link rel='stylesheet' href='/css/show_release.css'>
+ <%@ include file='header.jsp' %>
+ <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.min.js'></script>
+ <body>
+ <div class='container'>
+ <div class='row'>
+ <div class='col s12'>
+ <h4 id='section-header'>Test Plans</h4>
+ </div>
+ </div>
+ <div class='row' id='options'>
+ <c:forEach items='${planNames}' var='plan'>
+ <div>
+ <a href='/show_plan_release?plan=${plan}'>
+ <div class='col s12 card hoverable option valign-wrapper waves-effect'>
+ <span class='entry valign'>${plan}</span>
+ </div>
+ </a>
+ </div>
+ </c:forEach>
+ </div>
+ </div>
+ <%@ include file='footer.jsp' %>
+ </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_table.jsp b/src/main/webapp/WEB-INF/jsp/show_table.jsp
new file mode 100644
index 0000000..be71ee8
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_table.jsp
@@ -0,0 +1,340 @@
+<%--
+ ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+ ~ may not use this file except in compliance with the License. You may
+ ~ obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ ~ implied. See the License for the specific language governing
+ ~ permissions and limitations under the License.
+ --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+ <%@ include file="header.jsp" %>
+ <link type='text/css' href='/css/show_table.css' rel='stylesheet'>
+ <link type='text/css' href='/css/show_test_runs_common.css' rel='stylesheet'>
+ <link type='text/css' href='/css/search_header.css' rel='stylesheet'>
+ <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
+ <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+ <script src='js/search_header.js'></script>
+ <script type='text/javascript'>
+ google.charts.load('current', {'packages':['table', 'corechart']});
+ google.charts.setOnLoadCallback(drawGridTable);
+ google.charts.setOnLoadCallback(activateLogLinks);
+ google.charts.setOnLoadCallback(drawPieChart);
+ google.charts.setOnLoadCallback(function() {
+ $('.gradient').removeClass('gradient');
+ });
+
+ var search;
+
+ $(document).ready(function() {
+ search = $('#filter-bar').createSearchHeader('Module: ', '${testName}', refresh);
+ search.addFilter('Branch', 'branch', {
+ corpus: ${branches}
+ }, ${branch});
+ search.addFilter('Device', 'device', {
+ corpus: ${devices}
+ }, ${device});
+ search.addFilter('Device Build ID', 'deviceBuildId', {}, ${deviceBuildId});
+ search.addFilter('Test Build ID', 'testBuildId', {}, ${testBuildId});
+ search.addFilter('Host', 'hostname', {}, ${hostname});
+ search.addFilter('Passing Count', 'passing', {
+ type: 'number',
+ width: 's2'
+ }, ${passing});
+ search.addFilter('Non-Passing Count', 'nonpassing', {
+ type: 'number',
+ width: 's2'
+ }, ${nonpassing});
+ search.addRunTypeCheckboxes(${showPresubmit}, ${showPostsubmit});
+ search.display();
+
+ // disable buttons on load
+ if (!${hasNewer}) {
+ $('#newer-button').toggleClass('disabled');
+ }
+ if (!${hasOlder}) {
+ $('#older-button').toggleClass('disabled');
+ }
+ $('#treeLink').click(function() {
+ window.open('/show_tree?testName=${testName}', '_self');
+ });
+ $('#newer-button').click(prev);
+ $('#older-button').click(next);
+ });
+
+ // Actives the log links to display the log info modal when clicked.
+ function activateLogLinks() {
+ $('.info-btn').click(function(e) {
+ showLog(${logInfoMap}[$(this).data('col')]);
+ });
+ }
+
+ /** Displays a modal window with the specified log entries.
+ *
+ * @param logEntries Array of string arrays. Each entry in the outer array
+ * must contain (1) name string, and (2) url string.
+ */
+ function showLog(logEntries) {
+ if (!logEntries || logEntries.length == 0) return;
+
+ var logList = $('<ul class="collection"></ul>');
+ var entries = logEntries.reduce(function(acc, entry) {
+ if (!entry || entry.length == 0) return acc;
+ var link = '<a href="' + entry[1] + '"';
+ link += 'class="collection-item">' + entry[0] + '</li>';
+ return acc + link;
+ }, '');
+ logList.html(entries);
+ var infoContainer = $('#info-modal>.modal-content>.info-container');
+ infoContainer.empty();
+ logList.appendTo(infoContainer);
+ $('#info-modal').openModal();
+ }
+
+ // refresh the page to see the selected test types (pre-/post-submit)
+ function refresh() {
+ if($(this).hasClass('disabled')) return;
+ var link = '${pageContext.request.contextPath}' +
+ '/show_table?testName=${testName}' + search.args();
+ if (${unfiltered}) {
+ link += '&unfiltered=';
+ }
+ window.open(link,'_self');
+ }
+
+ // view older data
+ function next() {
+ if($(this).hasClass('disabled')) return;
+ var endTime = ${startTime};
+ var link = '${pageContext.request.contextPath}' +
+ '/show_table?testName=${testName}&endTime=' + endTime +
+ search.args();
+ if (${unfiltered}) {
+ link += '&unfiltered=';
+ }
+ window.open(link,'_self');
+ }
+
+ // view newer data
+ function prev() {
+ if($(this).hasClass('disabled')) return;
+ var startTime = ${endTime};
+ var link = '${pageContext.request.contextPath}' +
+ '/show_table?testName=${testName}&startTime=' + startTime +
+ search.args();
+ if (${unfiltered}) {
+ link += '&unfiltered=';
+ }
+ window.open(link,'_self');
+ }
+
+ // to draw pie chart
+ function drawPieChart() {
+ var topBuildResultCounts = ${topBuildResultCounts};
+ if (topBuildResultCounts.length < 1) {
+ return;
+ }
+ var resultNames = ${resultNamesJson};
+ var rows = resultNames.map(function(res, i) {
+ nickname = res.replace('TEST_CASE_RESULT_', '').replace('_', ' ')
+ .trim().toLowerCase();
+ return [nickname, parseInt(topBuildResultCounts[i])];
+ });
+ rows.unshift(['Result', 'Count']);
+
+ // Get CSS color definitions (or default to white)
+ var colors = resultNames.map(function(res) {
+ return $('.' + res).css('background-color') || 'white';
+ });
+
+ var data = google.visualization.arrayToDataTable(rows);
+ var options = {
+ is3D: false,
+ colors: colors,
+ fontName: 'Roboto',
+ fontSize: '14px',
+ legend: 'none',
+ tooltip: {showColorCode: true, ignoreBounds: true},
+ chartArea: {height: '90%'}
+ };
+
+ var chart = new google.visualization.PieChart(document.getElementById('pie-chart-div'));
+ chart.draw(data, options);
+ }
+
+ // table for grid data
+ function drawGridTable() {
+ var data = new google.visualization.DataTable();
+
+ // Add column headers.
+ headerRow = ${headerRow};
+ headerRow.forEach(function(d, i) {
+ var classNames = 'table-header-content';
+ if (i == 0) classNames += ' table-header-legend';
+ data.addColumn('string', '<span class="' + classNames + '">' +
+ d + '</span>');
+ });
+
+ var timeGrid = ${timeGrid};
+ var durationGrid = ${durationGrid};
+ var summaryGrid = ${summaryGrid};
+ var resultsGrid = ${resultsGrid};
+
+ // Format time grid to a formatted date
+ timeGrid = timeGrid.map(function(row) {
+ return row.map(function(cell, j) {
+ if (j == 0) return cell;
+ var time = moment(cell/1000);
+ // If today, don't display the date
+ if (time.isSame(moment(), 'd')) {
+ return time.format('H:mm:ssZZ');
+ } else {
+ return time.format('M/D/YY H:mm:ssZZ');
+ }
+ });
+ });
+
+ // Format duration grid to HH:mm:ss.SSS
+ durationGrid = durationGrid.map(function(row) {
+ return row.map(function(cell, j) {
+ if (j == 0) return cell;
+ return moment.utc(cell/1000).format("HH:mm:ss.SSS");
+ });
+ });
+
+ // add rows to the data.
+ data.addRows(timeGrid);
+ data.addRows(durationGrid);
+ data.addRows(summaryGrid);
+ data.addRows(resultsGrid);
+
+ var table = new google.visualization.Table(document.getElementById('grid-table-div'));
+ var classNames = {
+ headerRow : 'table-header',
+ headerCell : 'table-header-cell'
+ };
+ var options = {
+ showRowNumber: false,
+ alternatingRowStyle: true,
+ allowHtml: true,
+ frozenColumns: 1,
+ cssClassNames: classNames,
+ sort: 'disable'
+ };
+ table.draw(data, options);
+ }
+ </script>
+
+ <body>
+ <div class='wide container'>
+ <div class='row'>
+ <div class='col s12'>
+ <div class='card'>
+ <ul class='tabs'>
+ <li class='tab col s6'><a class='active'>Table</a></li>
+ <li class='tab col s6' id='treeLink'><a>Tree</a></li>
+ </ul>
+ </div>
+ <div id='filter-bar'></div>
+ </div>
+ <div class='col s7'>
+ <div class='col s12 card center-align'>
+ <div id='legend-wrapper'>
+ <c:forEach items='${resultNames}' var='res'>
+ <div class='center-align legend-entry'>
+ <c:set var='trimmed' value='${fn:replace(res, "TEST_CASE_RESULT_", "")}'/>
+ <c:set var='nickname' value='${fn:replace(trimmed, "_", " ")}'/>
+ <label for='${res}'>${nickname}</label>
+ <div id='${res}' class='${res} legend-bubble'></div>
+ </div>
+ </c:forEach>
+ </div>
+ </div>
+ <div id='profiling-container' class='col s12'>
+ <c:choose>
+ <c:when test='${empty profilingPointNames}'>
+ <div id='error-div' class='center-align card'><h5>${error}</h5></div>
+ </c:when>
+ <c:otherwise>
+ <ul id='profiling-body' class='collapsible' data-collapsible='accordion'>
+ <li>
+ <div class='collapsible-header'><i class='material-icons'>timeline</i>Profiling Graphs</div>
+ <div class='collapsible-body'>
+ <ul id='profiling-list' class='collection'>
+ <c:forEach items='${profilingPointNames}' var='pt'>
+ <c:set var='profPointArgs' value='testName=${testName}&profilingPoint=${pt}'/>
+ <c:set var='timeArgs' value='endTime=${endTime}'/>
+ <a href='/show_graph?${profPointArgs}&${timeArgs}'
+ class='collection-item profiling-point-name'>${pt}
+ </a>
+ </c:forEach>
+ </ul>
+ </div>
+ </li>
+ <li>
+ <a class='collapsible-link' href='/show_performance_digest?testName=${testName}'>
+ <div class='collapsible-header'><i class='material-icons'>toc</i>Performance Digest</div>
+ </a>
+ </li>
+ </ul>
+ </c:otherwise>
+ </c:choose>
+ </div>
+ </div>
+ <div class='col s5 valign-wrapper'>
+ <!-- pie chart -->
+ <div id='pie-chart-wrapper' class='col s12 valign center-align card'>
+ <h6 class='pie-chart-title'>Test Status for Device Build ID: ${topBuildId}</h6>
+ <div id='pie-chart-div'></div>
+ </div>
+ </div>
+ </div>
+
+ <div class='col s12'>
+ <div id='chart-holder' class='col s12 card'>
+ <!-- Grid tables-->
+ <div id='grid-table-div'></div>
+ </div>
+ </div>
+ <div id='newer-wrapper' class='page-button-wrapper fixed-action-btn'>
+ <a id='newer-button' class='btn-floating btn red waves-effect'>
+ <i class='large material-icons'>keyboard_arrow_left</i>
+ </a>
+ </div>
+ <div id='older-wrapper' class='page-button-wrapper fixed-action-btn'>
+ <a id='older-button' class='btn-floating btn red waves-effect'>
+ <i class='large material-icons'>keyboard_arrow_right</i>
+ </a>
+ </div>
+ </div>
+ <div id="help-modal" class="modal">
+ <div class="modal-content">
+ <h4>${searchHelpHeader}</h4>
+ <p>${searchHelpBody}</p>
+ </div>
+ <div class="modal-footer">
+ <a href="#!" class="modal-action modal-close waves-effect btn-flat">Close</a>
+ </div>
+ </div>
+ <div id="info-modal" class="modal">
+ <div class="modal-content">
+ <h4>Logs</h4>
+ <div class="info-container"></div>
+ </div>
+ <div class="modal-footer">
+ <a href="#!" class="modal-action modal-close waves-effect btn-flat">Close</a>
+ </div>
+ </div>
+ <%@ include file="footer.jsp" %>
+ </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_tree.jsp b/src/main/webapp/WEB-INF/jsp/show_tree.jsp
new file mode 100644
index 0000000..8d237a3
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_tree.jsp
@@ -0,0 +1,225 @@
+<%--
+ ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+ ~ may not use this file except in compliance with the License. You may
+ ~ obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ ~ implied. See the License for the specific language governing
+ ~ permissions and limitations under the License.
+ --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+ <%@ include file="header.jsp" %>
+ <link type='text/css' href='/css/show_test_runs_common.css' rel='stylesheet'>
+ <link type='text/css' href='/css/test_results.css' rel='stylesheet'>
+ <link rel='stylesheet' href='/css/search_header.css'>
+ <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
+ <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+ <script src='js/search_header.js'></script>
+ <script src='js/time.js'></script>
+ <script src='js/test_results.js'></script>
+ <script type='text/javascript'>
+ google.charts.load('current', {'packages':['table', 'corechart']});
+ google.charts.setOnLoadCallback(drawPieChart);
+
+ var search;
+
+ $(document).ready(function() {
+ search = $('#filter-bar').createSearchHeader('Module: ', '${testName}', refresh);
+ search.addFilter('Branch', 'branch', {
+ corpus: ${branches}
+ }, ${branch});
+ search.addFilter('Device', 'device', {
+ corpus: ${devices}
+ }, ${device});
+ search.addFilter('Device Build ID', 'deviceBuildId', {}, ${deviceBuildId});
+ search.addFilter('Test Build ID', 'testBuildId', {}, ${testBuildId});
+ search.addFilter('Host', 'hostname', {}, ${hostname});
+ search.addFilter('Passing Count', 'passing', {
+ type: 'number',
+ width: 's2'
+ }, ${passing});
+ search.addFilter('Non-Passing Count', 'nonpassing', {
+ type: 'number',
+ width: 's2'
+ }, ${nonpassing});
+ search.addRunTypeCheckboxes(${showPresubmit}, ${showPostsubmit});
+ search.display();
+
+ // disable buttons on load
+ if (!${hasNewer}) {
+ $('#newer-button').toggleClass('disabled');
+ }
+ if (!${hasOlder}) {
+ $('#older-button').toggleClass('disabled');
+ }
+ $('#tableLink').click(function() {
+ window.open('/show_table?testName=${testName}', '_self');
+ });
+ $('#newer-button').click(prev);
+ $('#older-button').click(next);
+ $('#test-results-container').showTests(${testRuns});
+ });
+
+ // refresh the page to see the selected test types (pre-/post-submit)
+ function refresh() {
+ if($(this).hasClass('disabled')) return;
+ var link = '${pageContext.request.contextPath}' +
+ '/show_tree?testName=${testName}' + search.args();
+ if (${unfiltered}) {
+ link += '&unfiltered=';
+ }
+ window.open(link,'_self');
+ }
+
+ // view older data
+ function next() {
+ if($(this).hasClass('disabled')) return;
+ var endTime = ${startTime};
+ var link = '${pageContext.request.contextPath}' +
+ '/show_tree?testName=${testName}&endTime=' + endTime +
+ search.args();
+ if (${unfiltered}) {
+ link += '&unfiltered=';
+ }
+ window.open(link,'_self');
+ }
+
+ // view newer data
+ function prev() {
+ if($(this).hasClass('disabled')) return;
+ var startTime = ${endTime};
+ var link = '${pageContext.request.contextPath}' +
+ '/show_tree?testName=${testName}&startTime=' + startTime +
+ search.args();
+ if (${unfiltered}) {
+ link += '&unfiltered=';
+ }
+ window.open(link,'_self');
+ }
+
+ // to draw pie chart
+ function drawPieChart() {
+ var topBuildResultCounts = ${topBuildResultCounts};
+ if (topBuildResultCounts.length < 1) {
+ return;
+ }
+ var resultNames = ${resultNamesJson};
+ var rows = resultNames.map(function(res, i) {
+ nickname = res.replace('TEST_CASE_RESULT_', '').replace('_', ' ')
+ .trim().toLowerCase();
+ return [nickname, parseInt(topBuildResultCounts[i])];
+ });
+ rows.unshift(['Result', 'Count']);
+
+ // Get CSS color definitions (or default to white)
+ var colors = resultNames.map(function(res) {
+ return $('.' + res).css('background-color') || 'white';
+ });
+
+ var data = google.visualization.arrayToDataTable(rows);
+ var options = {
+ is3D: false,
+ colors: colors,
+ fontName: 'Roboto',
+ fontSize: '14px',
+ legend: 'none',
+ tooltip: {showColorCode: true, ignoreBounds: true},
+ chartArea: {height: '90%'}
+ };
+
+ var chart = new google.visualization.PieChart(document.getElementById('pie-chart-div'));
+ chart.draw(data, options);
+ }
+ </script>
+
+ <body>
+ <div class='wide container'>
+ <div class='row'>
+ <div class='col s12'>
+ <div class='card'>
+ <ul class='tabs'>
+ <li class='tab col s6' id='tableLink'><a>Table</a></li>
+ <li class='tab col s6'><a class='active'>Tree</a></li>
+ </ul>
+ </div>
+ <div id='filter-bar'></div>
+ </div>
+ <div class='col s7'>
+ <div class='col s12 card center-align'>
+ <div id='legend-wrapper'>
+ <c:forEach items='${resultNames}' var='res'>
+ <div class='center-align legend-entry'>
+ <c:set var='trimmed' value='${fn:replace(res, "TEST_CASE_RESULT_", "")}'/>
+ <c:set var='nickname' value='${fn:replace(trimmed, "_", " ")}'/>
+ <label for='${res}'>${nickname}</label>
+ <div id='${res}' class='${res} legend-bubble'></div>
+ </div>
+ </c:forEach>
+ </div>
+ </div>
+ <div id='profiling-container' class='col s12'>
+ <c:choose>
+ <c:when test='${empty profilingPointNames}'>
+ <div id='error-div' class='center-align card'><h5>${error}</h5></div>
+ </c:when>
+ <c:otherwise>
+ <ul id='profiling-body' class='collapsible' data-collapsible='accordion'>
+ <li>
+ <div class='collapsible-header'><i class='material-icons'>timeline</i>Profiling Graphs</div>
+ <div class='collapsible-body'>
+ <ul id='profiling-list' class='collection'>
+ <c:forEach items='${profilingPointNames}' var='pt'>
+ <c:set var='profPointArgs' value='testName=${testName}&profilingPoint=${pt}'/>
+ <c:set var='timeArgs' value='endTime=${endTime}'/>
+ <a href='/show_graph?${profPointArgs}&${timeArgs}'
+ class='collection-item profiling-point-name'>${pt}
+ </a>
+ </c:forEach>
+ </ul>
+ </div>
+ </li>
+ <li>
+ <a class='collapsible-link' href='/show_performance_digest?testName=${testName}'>
+ <div class='collapsible-header'><i class='material-icons'>toc</i>Performance Digest</div>
+ </a>
+ </li>
+ </ul>
+ </c:otherwise>
+ </c:choose>
+ </div>
+ </div>
+ <div class='col s5 valign-wrapper'>
+ <!-- pie chart -->
+ <div id='pie-chart-wrapper' class='col s12 valign center-align card'>
+ <h6 class='pie-chart-title'>${topBuildId}</h6>
+ <div id='pie-chart-div'></div>
+ </div>
+ </div>
+ </div>
+
+ <div class='col s12' id='test-results-container'>
+ </div>
+ <div id='newer-wrapper' class='page-button-wrapper fixed-action-btn'>
+ <a id='newer-button' class='btn-floating btn red waves-effect'>
+ <i class='large material-icons'>keyboard_arrow_left</i>
+ </a>
+ </div>
+ <div id='older-wrapper' class='page-button-wrapper fixed-action-btn'>
+ <a id='older-button' class='btn-floating btn red waves-effect'>
+ <i class='large material-icons'>keyboard_arrow_right</i>
+ </a>
+ </div>
+ </div>
+ <%@ include file="footer.jsp" %>
+ </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..e5ed62c
--- /dev/null
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,197 @@
+<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
+<!--
+Copyright 2016 Google Inc. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<servlet>
+ <servlet-name>dashboard_main</servlet-name>
+ <servlet-class>com.android.vts.servlet.DashboardMainServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>show_release</servlet-name>
+ <servlet-class>com.android.vts.servlet.ShowReleaseServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>show_coverage_overview</servlet-name>
+ <servlet-class>com.android.vts.servlet.ShowCoverageOverviewServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>show_tree</servlet-name>
+ <servlet-class>com.android.vts.servlet.ShowTreeServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>show_table</servlet-name>
+ <servlet-class>com.android.vts.servlet.ShowTableServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>show_graph</servlet-name>
+ <servlet-class>com.android.vts.servlet.ShowGraphServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>show_plan_release</servlet-name>
+ <servlet-class>com.android.vts.servlet.ShowPlanReleaseServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>show_plan_run</servlet-name>
+ <servlet-class>com.android.vts.servlet.ShowPlanRunServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>show_performance_digest</servlet-name>
+ <servlet-class>com.android.vts.servlet.ShowPerformanceDigestServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>show_coverage</servlet-name>
+ <servlet-class>com.android.vts.servlet.ShowCoverageServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>datastore</servlet-name>
+ <servlet-class>com.android.vts.api.DatastoreRestServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>test_run</servlet-name>
+ <servlet-class>com.android.vts.api.TestRunRestServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>favorites</servlet-name>
+ <servlet-class>com.android.vts.api.UserFavoriteRestServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>bigtable_legacy</servlet-name>
+ <servlet-class>com.android.vts.api.BigtableLegacyJsonServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>vts_alert_job</servlet-name>
+ <servlet-class>com.android.vts.servlet.VtsAlertJobServlet</servlet-class>
+</servlet>
+
+<servlet>
+ <servlet-name>vts_performance_job</servlet-name>
+ <servlet-class>com.android.vts.servlet.VtsPerformanceJobServlet</servlet-class>
+</servlet>
+
+<servlet-mapping>
+ <servlet-name>dashboard_main</servlet-name>
+ <url-pattern>/</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>show_release</servlet-name>
+ <url-pattern>/show_release/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>show_coverage_overview</servlet-name>
+ <url-pattern>/show_coverage_overview/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>show_tree</servlet-name>
+ <url-pattern>/show_tree/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>show_table</servlet-name>
+ <url-pattern>/show_table/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>show_graph</servlet-name>
+ <url-pattern>/show_graph/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>show_plan_release</servlet-name>
+ <url-pattern>/show_plan_release/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>show_plan_run</servlet-name>
+ <url-pattern>/show_plan_run/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>show_performance_digest</servlet-name>
+ <url-pattern>/show_performance_digest/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>show_coverage</servlet-name>
+ <url-pattern>/show_coverage/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>bigtable_legacy</servlet-name>
+ <url-pattern>/api/bigtable/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>datastore</servlet-name>
+ <url-pattern>/api/datastore/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>test_run</servlet-name>
+ <url-pattern>/api/test_run/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>favorites</servlet-name>
+ <url-pattern>/api/favorites/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>vts_alert_job</servlet-name>
+ <url-pattern>/cron/vts_alert_job/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+ <servlet-name>vts_performance_job</servlet-name>
+ <url-pattern>/cron/vts_performance_job/*</url-pattern>
+</servlet-mapping>
+
+<security-constraint>
+ <web-resource-collection>
+ <web-resource-name>cron</web-resource-name>
+ <url-pattern>/cron/*</url-pattern>
+ </web-resource-collection>
+ <auth-constraint>
+ <role-name>admin</role-name>
+ </auth-constraint>
+</security-constraint>
+
+<security-constraint>
+ <web-resource-collection>
+ <web-resource-name>all</web-resource-name>
+ <url-pattern>/show_*</url-pattern>
+ </web-resource-collection>
+ <auth-constraint>
+ <role-name>*</role-name>
+ </auth-constraint>
+</security-constraint>
+</web-app>
diff --git a/src/main/webapp/css/common.css b/src/main/webapp/css/common.css
new file mode 100644
index 0000000..974f129
--- /dev/null
+++ b/src/main/webapp/css/common.css
@@ -0,0 +1,25 @@
+/* Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+.container {
+ min-height: 80%;
+}
+
+@media only screen and (min-width: 993px) {
+ .wide.container {
+ width: 80%;
+ max-width: 1600px;
+ }
+}
diff --git a/src/main/webapp/css/dashboard_main.css b/src/main/webapp/css/dashboard_main.css
new file mode 100644
index 0000000..e6e899f
--- /dev/null
+++ b/src/main/webapp/css/dashboard_main.css
@@ -0,0 +1,109 @@
+/* Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+#edit-button-wrapper {
+ bottom: 25px;
+ right: 25px;
+}
+
+.input-field {
+ margin-bottom: 50px;
+}
+
+#add-button-wrapper {
+ margin-top: 10px;
+ height: 61px;
+}
+
+.btn-flat.clear-button {
+ margin-top: 8px;
+ user-select: none;
+ color: grey;
+}
+
+.row .col.card.option {
+ padding: 6px 15px 6px 15px;
+ margin: 5px 0;
+ border-radius: 25px;
+}
+
+#error-container {
+ padding-top: 50px;
+ padding-bottom: 50px;
+}
+
+.entry {
+ font-size: 20px;
+ font-weight: 300;
+ position: relative;
+}
+
+.indicator {
+ color: white;
+ font-size: 12px;
+ font-weight: bold;
+ padding: 1px 6px;
+ position: absolute;
+ right: 0;
+ min-width: 40px;
+ border-radius: 10px;
+ margin-top: 5px;
+}
+
+#show-button {
+ border-radius: 100px;
+}
+
+#show-button-arrow {
+ margin-left: 3px;
+}
+
+#section-header {
+ cursor: default;
+ user-select: none;
+ color: #ee6e73;
+}
+
+#section-header:after {
+ border: 1px solid #ee6e73;
+ margin-top: 10px;
+ margin-bottom: 0;
+ display: block;
+ content: " ";
+}
+
+.ui-menu {
+ overflow-y: auto;
+ z-index: 100;
+ max-height: 50%;
+}
+
+.ui-menu-item {
+ font-size: 16px;
+ padding: 4px 10px;
+ transition: background-color .25s;
+}
+
+.ui-menu-item:hover {
+ background-color: #e0f2f1;
+}
+
+.ui-menu-item:active {
+ background-color: #b2dfdb;
+}
+
+.ui-helper-hidden-accessible {
+ display: none;
+}
diff --git a/src/main/webapp/css/datepicker.css b/src/main/webapp/css/datepicker.css
new file mode 100644
index 0000000..b98dd80
--- /dev/null
+++ b/src/main/webapp/css/datepicker.css
@@ -0,0 +1,70 @@
+/* Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+.ui-datepicker table {
+ font-family: Roboto !important;
+ font-size: 13px !important;
+ white-space: nowrap !important;
+}
+
+#ui-datepicker-div {
+ font-family: "Roboto", sans-serif;
+ padding: 0;
+}
+
+.ui-corner-all {
+ border-bottom-right-radius: 0 !important;
+ border-bottom-left-radius: 0 !important;
+ border-top-right-radius: 0 !important;
+ border-top-left-radius: 0 !important;
+}
+
+.ui-datepicker td span.ui-state-default, .ui-datepicker td a.ui-state-default {
+ text-align: center;
+ padding: 0.7em 0.4em;
+ border: none;
+ border-radius: 10em;
+ background: none;
+}
+
+.ui-datepicker td span.ui-state-hover, .ui-datepicker td a.ui-state-hover {
+ background: #e0f2f1;
+}
+
+.ui-datepicker td span.ui-state-active, .ui-datepicker td a.ui-state-active {
+ color: white;
+ font-weight: 600;
+ background: #009688;
+}
+
+.ui-datepicker-header.ui-widget-header.ui-helper-clearfix.ui-corner-all {
+ background: #009688;
+ border: none;
+ color: white;
+}
+
+.ui-datepicker-next.ui-state-hover.ui-datepicker-next-hover {
+ right: 2px;
+ top: 2px;
+}
+
+.ui-datepicker-prev.ui-state-hover.ui-datepicker-prev-hover {
+ left: 2px;
+ top: 2px;
+}
+
+.ui-datepicker-next.ui-corner-all.ui-state-hover, .ui-datepicker-prev.ui-corner-all.ui-state-hover {
+ background: none;
+ border: none;
+}
diff --git a/src/main/webapp/css/navbar.css b/src/main/webapp/css/navbar.css
new file mode 100644
index 0000000..9395d1b
--- /dev/null
+++ b/src/main/webapp/css/navbar.css
@@ -0,0 +1,69 @@
+/* Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+nav#navbar {
+ height: auto;
+ margin-bottom: 30px;
+ user-select: none;
+}
+nav#navbar .nav-wrapper {
+ height: auto;
+}
+nav#navbar ul.nav-list {
+ display: inline-block;
+}
+nav#navbar ul li {
+ transition: background-color .3s;
+ float: left;
+ padding: 0;
+}
+nav#navbar ul li.active {
+ background-color: rgba(0,0,0,0.1);
+}
+nav#navbar ul li a:hover, nav#navbar ul li.active {
+ background-color: #ea454b;
+}
+nav#navbar ul a.nav-list-item {
+ font-size: 1.2rem;
+}
+nav#navbar #nav-sublist {
+ line-height: 35px;
+ background: white;
+ padding-left: 15px;
+ width: 100%;
+ display: inline-block;
+ position: relative;
+}
+nav#navbar #nav-sublist .breadcrumb.nav-sublist-item {
+ font-size: 15px;
+ font-weight: 400;
+ color: rgba(238, 110, 115, 0.85);
+ line-height: 35px;
+}
+nav#navbar #nav-sublist .breadcrumb.nav-sublist-item:last-child {
+ color: rgb(238, 110, 115);
+ font-weight: 500;
+}
+nav#navbar #nav-sublist .breadcrumb.nav-sublist-item::before {
+ font-size: 22px;
+ color: rgba(238, 110, 115, 0.85);
+ line-height: 35px;
+ margin: 0 5px;
+}
+nav#navbar #dropdown-button {
+ font-style: italic;
+ color: rgba(255, 255, 255, 0.75);
+ margin-left: 0;
+ margin-top: 0;
+}
diff --git a/src/main/webapp/css/plan_runs.css b/src/main/webapp/css/plan_runs.css
new file mode 100644
index 0000000..3c9eeb9
--- /dev/null
+++ b/src/main/webapp/css/plan_runs.css
@@ -0,0 +1,30 @@
+/* Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+.plan-run-metadata {
+ display: inline-block;
+ font-size: 13px;
+ line-height: 16px;
+ padding: 10px;
+}
+.release-entry {
+ border-radius: 5px 5px 10px 10px;
+}
+.counter {
+ color: white;
+ font-size: 12px;
+ font-weight: bold;
+ display: block;
+ border-radius: 0 0 10px 10px;
+} \ No newline at end of file
diff --git a/src/main/webapp/css/search_header.css b/src/main/webapp/css/search_header.css
new file mode 100644
index 0000000..90573e1
--- /dev/null
+++ b/src/main/webapp/css/search_header.css
@@ -0,0 +1,80 @@
+/* Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+.row.card.search-bar {
+ margin-left: 0;
+ margin-right: 0;
+}
+.search-bar .header-wrapper {
+ border-bottom: 1px solid rgb(221, 221, 221);;
+}
+.search-bar .section-header {
+ color: rgb(97, 97, 97);
+ font-weight: 300;
+ padding: 15px 20px 15px;
+ margin: 0;
+ cursor: default;
+ user-select: none;
+ display: inline-block;
+}
+.search-bar .section-header b {
+ color: #ee6e73;
+}
+.search-bar .search-icon-wrapper {
+ text-align: center;
+ display: inline-block;
+ position: absolute;
+ right: 0;
+ height: 57px;
+ width: 57px;
+ cursor: pointer;
+ user-select: none;
+}
+.search-bar .search-icon-wrapper i {
+ line-height: 57px;
+ color: rgb(97, 97, 97);
+}
+.search-bar .search-wrapper .refresh-wrapper a {
+ float: right;
+ margin-top: 17px;
+}
+.search-bar .input-field input[type=text].invalid {
+ color: #F44336;
+}
+.search-bar .search-wrapper .run-type-wrapper {
+ margin: 20px 0;
+}
+.search-bar .search-wrapper .run-type-wrapper [type="checkbox"]+label {
+ margin-right: 35px;
+}
+.search-bar-menu .ui-menu {
+ overflow-y: auto;
+ z-index: 100;
+ max-height: 50%;
+}
+.search-bar-menu .ui-menu-item {
+ font-size: 16px;
+ padding: 4px 10px;
+ transition: background-color .25s;
+}
+.search-bar-menu .ui-menu-item:hover {
+ background-color: #e0f2f1;
+}
+
+.search-bar-menu .ui-menu-item:active {
+ background-color: #b2dfdb;
+}
+.search-bar-menu .ui-helper-hidden-accessible {
+ display: none;
+}
diff --git a/src/main/webapp/css/show_coverage.css b/src/main/webapp/css/show_coverage.css
new file mode 100644
index 0000000..c187fd3
--- /dev/null
+++ b/src/main/webapp/css/show_coverage.css
@@ -0,0 +1,104 @@
+/* Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+.collapsible.popout {
+ margin-bottom: 50px;
+}
+
+.table {
+ font-size: 12px;
+ border: none;
+ width: 100%;
+ word-spacing: 4px;
+ font-family: monospace;
+ white-space: PRE;
+ border-collapse: collapse;
+}
+
+.section-title {
+ margin-left: 24px;
+}
+
+.html-container {
+ padding: 25px 25px;
+}
+
+.login-button {
+ border: 1px solid gray;
+ color: gray;
+ border-radius: 15px;
+ padding: 4px 15px;
+ cursor: pointer;
+}
+
+td {
+ padding: 0;
+}
+
+.count {
+ white-space: nowrap;
+ text-align: right;
+ border-right: 1px solid black;
+ padding-right: 5px;
+}
+
+.line_no {
+ padding-left: 35px;
+ white-space: nowrap;
+ padding-right: 5px;
+ border-right: 1px dotted gray;
+}
+
+.code {
+ padding-left: 10px;
+ width: 99%;
+}
+
+.indicator {
+ display: inline-block;
+ width: 50px;
+ margin-top: 12px;
+ line-height: 18px;
+ border-radius: 10px;
+ padding: 2px 5px;
+ text-align: center;
+ font-size: 12px;
+ font-weight: bold;
+ color: white;
+}
+
+.total-count {
+ margin-top: 12px;
+ margin-right: 8px;
+ padding-right: 5px;
+ line-height: 22px;
+ border-right: 1px solid gray;
+ font-size: 12px;
+ font-weight: bold;
+ color: gray;
+}
+
+.uncovered {
+ background-color: LightPink;
+}
+
+.covered {
+ background-color: LightGreen;
+}
+
+.source-name {
+ margin-top: -25px;
+ padding-left: 45px;
+}
diff --git a/src/main/webapp/css/show_graph.css b/src/main/webapp/css/show_graph.css
new file mode 100644
index 0000000..51e4c04
--- /dev/null
+++ b/src/main/webapp/css/show_graph.css
@@ -0,0 +1,70 @@
+/* Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+#download {
+ bottom: 25px;
+ right: 25px;
+}
+
+#header-container {
+ padding: 50px;
+ padding-bottom: 30px;
+}
+
+#device-select-wrapper {
+ margin-top: 0;
+}
+
+#date-container {
+ padding: 0 50px;
+ padding-bottom: 25px;
+}
+
+#load {
+ margin-top: 5px;
+}
+
+.profiling-name {
+ font-weight: 200;
+ font-style: italic;
+ font-size: 1.4rem
+}
+
+#error-container {
+ padding-top: 25px;
+ padding-bottom: 25px;
+}
+
+.graph-wrapper {
+ padding: 30px;
+}
+
+.graph {
+ height: 500px;
+ padding-bottom: 30px;
+}
+
+.percentile-table {
+ width: auto;
+ margin: auto;
+ margin-top: 20px;
+}
+
+.percentile-table td, th{
+ font-size: 11px;
+ text-align: center;
+ padding: 5px 10px;
+}
+
diff --git a/src/main/webapp/css/show_performance_digest.css b/src/main/webapp/css/show_performance_digest.css
new file mode 100644
index 0000000..5d0c0e6
--- /dev/null
+++ b/src/main/webapp/css/show_performance_digest.css
@@ -0,0 +1,82 @@
+/* Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+#header-container {
+ padding: 25px;
+}
+
+#load {
+ margin-top: 15px;
+}
+
+#device-select-wrapper {
+ margin-top: 9px;
+}
+
+#date {
+ margin-bottom: 0;
+ margin-top: 10px;
+}
+
+div.col.card.summary {
+ padding: 0 20px 20px;
+}
+
+.profiling-name {
+ font-weight: 200;
+ font-style: italic;
+ font-size: 1.4rem
+}
+
+.profiling-subtitle {
+ font-style: italic;
+ font-size: 12px;
+ margin-left: 2px;
+}
+
+.summary table {
+ width: 100%;
+ border-collapse: collapse;
+ border: 1px solid black;
+ font-size: 12px;
+ font-family: Roboto !important;
+}
+
+.summary table td, th {
+ padding: 2px;
+}
+
+.section-label {
+ border: 1px solid black;
+}
+
+.axis-label {
+ border-top: 1px solid lightgray;
+ border-right: 1px solid black;
+ text-align: right;
+}
+
+.cell {
+ border-top: 1px solid lightgray;
+ text-align: right;
+}
+
+.inner-cell {
+ border-right: 1px solid lightgray;
+}
+
+.outer-cell {
+ border-right: 1px solid black;
+}
diff --git a/src/main/webapp/css/show_plan_release.css b/src/main/webapp/css/show_plan_release.css
new file mode 100644
index 0000000..f55b8da
--- /dev/null
+++ b/src/main/webapp/css/show_plan_release.css
@@ -0,0 +1,23 @@
+/* Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+.page-button-wrapper {
+ top: 50%;
+ bottom: auto;
+ padding: 0;
+}
+#newer-wrapper {
+ left: 23px;
+ right: auto;
+}
diff --git a/src/main/webapp/css/show_release.css b/src/main/webapp/css/show_release.css
new file mode 100644
index 0000000..1f1758b
--- /dev/null
+++ b/src/main/webapp/css/show_release.css
@@ -0,0 +1,40 @@
+/* Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+.row .col.card.option {
+ padding: 10px 15px 10px 25px;
+ margin: 5px 0;
+ border-radius: 25px;
+}
+.entry {
+ font-size: 20px;
+ font-weight: 300;
+ position: relative;
+}
+#show-button-arrow {
+ margin-left: 3px;
+}
+#section-header {
+ cursor: default;
+ user-select: none;
+ color: #ee6e73;
+}
+#section-header:after {
+ border: 1px solid #ee6e73;
+ margin-top: 10px;
+ margin-bottom: 0;
+ display: block;
+ content: " ";
+}
diff --git a/src/main/webapp/css/show_table.css b/src/main/webapp/css/show_table.css
new file mode 100644
index 0000000..76d4379
--- /dev/null
+++ b/src/main/webapp/css/show_table.css
@@ -0,0 +1,156 @@
+/* Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+table {
+ font-family: Roboto !important;
+ font-size: 13px !important;
+ white-space: nowrap !important;
+}
+.table-header-cell {
+ background-color: white;
+ transition: max-width 1s;
+ max-width: 150px;
+}
+.table-header-cell:hover {
+ transition-delay: 0.5s;
+ max-width: 1000px;
+}
+.table-header-content.table-header-legend {
+ max-width: none;
+}
+.table-header-content {
+ font-weight: initial;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ max-width: inherit;
+ width: 100%;
+}
+.page-button-wrapper {
+ top: 50%;
+ bottom: auto;
+ padding: 0;
+}
+#newer-wrapper {
+ left: 23px;
+ right: auto;
+}
+#chart-holder {
+ padding: 30px 5px;
+}
+#pie-chart-div {
+ width: 100%;
+ height: 300px;
+}
+#profiling-container {
+ padding: 0;
+}
+#error-div {
+ padding: 30px 25px;
+}
+#profiling-body {
+ border: none;
+}
+#profiling-list {
+ max-height: 200px;
+ overflow-y: scroll;
+}
+.collapsible-header {
+ user-select: none;
+}
+.collapsible-link {
+ color: inherit;
+}
+.collection a.collection-item.profiling-point-name {
+ color: #616161;
+ font-size: 13px;
+ padding: 2px 0 2px 25px;
+}
+#legend-wrapper {
+ display: inline-block;
+ padding: 10px 15px 12px;
+}
+.btn.inline-btn {
+ border-radius: 50px;
+ height: auto;
+ line-height: inherit;
+ padding: 1px;
+ margin-left: 2px;
+}
+i.material-icons.inline-icon {
+ font-size: inherit;
+}
+a.legend-circle {
+ width: 15px;
+ height: 15px;
+ padding: 0;
+ border-radius: 15px;
+}
+.legend-header-cell {
+ text-transform: capitalize;
+}
+.legend-entry {
+ display: inline-block;
+ margin: 0 5px;
+ min-width: 50px;
+}
+.legend-bubble {
+ border-radius: 20px;
+ height: 20px;
+ width: 20px;
+ margin: 0 auto;
+}
+#pie-chart-wrapper {
+ padding: 25px 0;
+}
+.pie-chart-title {
+ cursor: default;
+}
+div.status-icon {
+ width: 10px;
+ height: 10px;
+ border-radius: 10px;
+ display: inline-block;
+ margin-left: 5px;
+}
+.test-case-status {
+ border-radius: 50px;
+ display: inline-block;
+ height: 100%;
+ width: 100%;
+}
+.test-case-status.width-1 {
+ width: calc(100% - 18px);
+}
+.TEST_CASE_RESULT_PASS {
+ background-color: #7FFF00;
+}
+.TEST_CASE_RESULT_FAIL {
+ background-color: #ff4d4d;
+}
+.TEST_CASE_RESULT_SKIP {
+ background-color: #A8A8A8;
+}
+.TEST_CASE_RESULT_EXCEPTION {
+ background-color: black;
+}
+.TEST_CASE_RESULT_TIMEOUT {
+ background-color: #9900CC;
+}
+.UNKNOWN_RESULT {
+ background-color: white;
+ border: 1px #A8A8A8 solid;
+}
diff --git a/src/main/webapp/css/show_test_runs_common.css b/src/main/webapp/css/show_test_runs_common.css
new file mode 100644
index 0000000..63d5301
--- /dev/null
+++ b/src/main/webapp/css/show_test_runs_common.css
@@ -0,0 +1,136 @@
+/* Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+.page-button-wrapper {
+ top: 50%;
+ bottom: auto;
+ padding: 0;
+}
+#newer-wrapper {
+ left: 23px;
+ right: auto;
+}
+#chart-holder {
+ padding: 30px 5px;
+}
+#pie-chart-div {
+ width: 100%;
+ height: 300px;
+}
+.row .col div.pie-chart-div {
+ width: 100%;
+ height: 255px;
+}
+#profiling-container {
+ padding: 0;
+}
+#error-div {
+ padding: 30px 25px;
+}
+#profiling-body {
+ border: none;
+}
+#profiling-list {
+ max-height: 200px;
+ overflow-y: scroll;
+}
+.collapsible-header {
+ user-select: none;
+}
+.collapsible-link {
+ color: inherit;
+}
+.collection a.collection-item.profiling-point-name {
+ color: #616161;
+ font-size: 13px;
+ padding: 2px 0 2px 25px;
+}
+#legend-wrapper {
+ display: inline-block;
+ padding: 10px 15px 12px;
+}
+.btn.inline-btn {
+ border-radius: 50px;
+ height: auto;
+ line-height: inherit;
+ padding: 1px;
+ margin-left: 2px;
+}
+i.material-icons.inline-icon {
+ font-size: inherit;
+}
+a.legend-circle {
+ width: 15px;
+ height: 15px;
+ padding: 0;
+ border-radius: 15px;
+}
+.legend-header-cell {
+ text-transform: capitalize;
+}
+.legend-entry {
+ display: inline-block;
+ margin: 0 5px;
+ min-width: 50px;
+}
+.legend-bubble {
+ border-radius: 20px;
+ height: 20px;
+ width: 20px;
+ margin: 0 auto;
+}
+#pie-chart-wrapper, .row .col.pie-chart-wrapper {
+ padding: 25px 0;
+}
+.pie-chart-title {
+ cursor: default;
+}
+div.status-icon {
+ width: 10px;
+ height: 10px;
+ border-radius: 10px;
+ display: inline-block;
+ margin-left: 5px;
+}
+.test-case-status {
+ border-radius: 50px;
+ display: inline-block;
+ height: 100%;
+ width: 100%;
+}
+.test-case-status.width-1 {
+ width: calc(100% - 18px);
+}
+.TEST_CASE_RESULT_PASS {
+ background-color: #4CAF50;
+}
+.TEST_CASE_RESULT_FAIL {
+ background-color: #F44336;
+}
+.TEST_CASE_RESULT_SKIP {
+ background-color: #A8A8A8;
+}
+.TEST_CASE_RESULT_EXCEPTION {
+ background-color: black;
+}
+.TEST_CASE_RESULT_TIMEOUT {
+ background-color: #9900CC;
+}
+.UNKNOWN_RESULT {
+ background-color: white;
+ border: 1px #A8A8A8 solid;
+}
+.tabs > div.indicator {
+ height: 3px;
+}
diff --git a/src/main/webapp/css/test_results.css b/src/main/webapp/css/test_results.css
new file mode 100644
index 0000000..f717e79
--- /dev/null
+++ b/src/main/webapp/css/test_results.css
@@ -0,0 +1,78 @@
+/* Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+li.test-run-container.active {
+ border-radius: 0 0 10px 10px;
+}
+.collapsible-header {
+ user-select: none;
+}
+.collapsible-header.disabled {
+ pointer-events: none;
+}
+.collapsible-header.test-run {
+ position: relative;
+}
+.test-run-metadata {
+ font-size: 13px;
+ line-height: 15px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ position: relative;
+ display: inline-block;
+ cursor: text;
+ user-select: initial;
+}
+.test-results.row {
+ margin: 0;
+ border-radius: 0 0 10px 10px;
+}
+.test-case-container {
+ border: 1px solid lightgray;
+ background: white;
+ padding: 10px;
+ margin-bottom: 25px;
+ max-height: 80%;
+ overflow: auto;
+}
+.indicator {
+ color: white;
+ font-size: 12px;
+ line-height: 20px;
+ font-weight: bold;
+ padding: 1px 6px;
+ margin-top: 10px;
+ min-width: 40px;
+ border-radius: 10px;
+}
+.indicator.padded {
+ margin-right: 5px;
+}
+.material-icons.expand-arrow {
+ right: 3px;
+ bottom: 0px;
+ position: absolute;
+ transition: transform 0.2s;
+}
+.rotate {
+ transform: rotate(-180deg);
+}
+i.material-icons.inline-icon {
+ font-size: inherit;
+}
+.test-run-label {
+ font-size: 18px;
+ line-height: 35px;
+ font-weight: 300;
+}
diff --git a/src/main/webapp/js/plan_runs.js b/src/main/webapp/js/plan_runs.js
new file mode 100644
index 0000000..6898389
--- /dev/null
+++ b/src/main/webapp/js/plan_runs.js
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may
+ * obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+(function ($, moment) {
+
+ /**
+ * Display test plan metadata in a vertical popout.
+ * @param container The jquery object in which to insert the plan metadata.
+ * @param metadataList The list of metadata objects to render on the display.
+ */
+ function renderCard(container, entry) {
+ var card = $('<div class="col s12 m6 l4"></div>');
+ card.appendTo(container);
+ var div = $('<div class="hoverable card release-entry"></div>');
+ var startTime = entry.testPlanRun.startTimestamp;
+ var endTime = entry.testPlanRun.endTimestamp;
+ div.appendTo(card);
+ var span = $('<span></span>');
+ span.addClass('plan-run-metadata');
+ span.appendTo(div);
+ $('<b></b>').text(entry.deviceInfo).appendTo(span);
+ span.append('<br>');
+ $('<b></b>').text('VTS Build: ').appendTo(span);
+ span.append(entry.testPlanRun.testBuildId).append('<br>');
+ var timeString = (
+ moment().renderTime(startTime, false) + ' - ' +
+ moment().renderTime(endTime, true) + ' (' +
+ moment().renderDuration(endTime - startTime) + ')');
+ span.append(timeString);
+ var counter = $('<span></span>');
+ var color = entry.testPlanRun.failCount > 0 ? 'red' : 'green';
+ counter.addClass('counter center ' + color);
+ counter.append(
+ entry.testPlanRun.passCount + '/' +
+ (entry.testPlanRun.passCount + entry.testPlanRun.failCount));
+ counter.appendTo(div);
+ div.click(function () {
+ window.location.href = (
+ '/show_plan_run?plan=' + entry.testPlanRun.testPlanName +
+ '&time=' + entry.testPlanRun.startTimestamp);
+ })
+ }
+
+ $.fn.showPlanRuns = function(data) {
+ var self = $(this);
+ data.forEach(function (entry) {
+ renderCard(self, entry);
+ })
+ }
+
+})(jQuery, moment);
diff --git a/src/main/webapp/js/search_header.js b/src/main/webapp/js/search_header.js
new file mode 100644
index 0000000..0244815
--- /dev/null
+++ b/src/main/webapp/js/search_header.js
@@ -0,0 +1,242 @@
+/**
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may
+ * obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+(function ($) {
+
+ function _validate(input, valueSet) {
+ var value = input.val();
+ if (valueSet.has(value) || !value) {
+ input.removeClass('invalid');
+ } else {
+ input.addClass('invalid');
+ }
+ }
+
+ function _createInput(key, config) {
+ var value = config.value;
+ var values = config.options.corpus;
+ var displayName = config.displayName;
+ var width = config.options.width || 's4';
+ var div = $('<div class="input-field col"></div>');
+ div.addClass(width);
+ var input = $('<input class="filter-input"></input>');
+ input.attr('type', config.options.type || 'text');
+ input.appendTo(div);
+ var label = $('<label></label>').text(displayName).appendTo(div);
+ if (value) {
+ input.attr('value', value);
+ label.addClass('active');
+ }
+ input.focusout(function() {
+ config.value = input.val();
+ });
+ if (values && values.length > 0) {
+ var valueSet = new Set(values);
+ input.sizedAutocomplete({
+ source: values,
+ classes: {
+ 'ui-autocomplete': 'card search-bar-menu'
+ }
+ });
+ input.focusout(function() {
+ _validate(input, valueSet);
+ });
+ }
+ if (values && values.length > 0 && value) {
+ _validate(input, valueSet);
+ }
+ return div;
+ }
+
+ function _verifyCheckboxes(checkboxes, refreshObject) {
+ var oneChecked = checkboxes.presubmit || checkboxes.postsubmit;
+ if (!oneChecked) {
+ refreshObject.addClass('disabled');
+ } else {
+ refreshObject.removeClass('disabled');
+ }
+ }
+
+ function _createRunTypeBoxes(checkboxes, refreshObject) {
+ var container = $('<div class="run-type-wrapper col s12"></div>');
+ var presubmit = $('<input type="checkbox" id="presubmit"></input>');
+ presubmit.appendTo(container);
+ if (checkboxes.presubmit) {
+ presubmit.prop('checked', true);
+ }
+ container.append('<label for="presubmit">Presubmit</label>');
+ var postsubmit = $('<input type="checkbox" id="postsubmit"></input>');
+ postsubmit.appendTo(container);
+ if (checkboxes.postsubmit) {
+ postsubmit.prop('checked', true);
+ }
+ container.append('<label for="postsubmit">Postsubmit</label>');
+ presubmit.change(function() {
+ checkboxes.presubmit = presubmit.prop('checked');
+ _verifyCheckboxes(checkboxes, refreshObject);
+ });
+ postsubmit.change(function() {
+ checkboxes.postsubmit = postsubmit.prop('checked');
+ _verifyCheckboxes(checkboxes, refreshObject);
+ });
+ return container;
+ }
+
+ function _expand(
+ container, filters, checkboxes, onRefreshCallback, animate=true) {
+ var wrapper = $('<div class="search-wrapper"></div>');
+ var col = $('<div class="col s9"></div>');
+ col.appendTo(wrapper);
+ Object.keys(filters).forEach(function(key) {
+ col.append(_createInput(key, filters[key]));
+ });
+ var refreshCol = $('<div class="col s3 refresh-wrapper"></div>');
+ var refresh = $('<a class="btn-floating btn-medium red right waves-effect waves-light"></a>')
+ .append($('<i class="medium material-icons">cached</i>'))
+ .appendTo(refreshCol);
+ refresh.click(onRefreshCallback);
+ refreshCol.appendTo(wrapper);
+ if (Object.keys(checkboxes).length > 0) {
+ col.append(_createRunTypeBoxes(checkboxes, refresh));
+ }
+ if (animate) {
+ wrapper.hide().appendTo(container).slideDown({
+ duration: 350,
+ easing: "easeOutQuart",
+ queue: false
+ });
+ } else {
+ wrapper.appendTo(container);
+ }
+ }
+
+ function _renderHeader(
+ container, label, value, filters, checkboxes, expand, onRefreshCallback) {
+ var div = $('<div class="row card search-bar"></div>');
+ var wrapper = $('<div class="header-wrapper"></div>');
+ var header = $('<h5 class="section-header"></h5>');
+ $('<b></b>').text(label).appendTo(header);
+ $('<span></span>').text(value).appendTo(header);
+ header.appendTo(wrapper);
+ var iconWrapper = $('<div class="search-icon-wrapper"></div>');
+ $('<i class="material-icons">search</i>').appendTo(iconWrapper);
+ iconWrapper.appendTo(wrapper);
+ wrapper.appendTo(div);
+ if (expand) {
+ _expand(div, filters, checkboxes, onRefreshCallback, false);
+ } else {
+ var expanded = false;
+ iconWrapper.click(function() {
+ if (expanded) return;
+ expanded = true;
+ _expand(div, filters, checkboxes, onRefreshCallback);
+ });
+ }
+ div.appendTo(container);
+ }
+
+ function _addFilter(filters, displayName, keyName, options, defaultValue) {
+ filters[keyName] = {};
+ filters[keyName].displayName = displayName;
+ filters[keyName].value = defaultValue;
+ filters[keyName].options = options;
+ }
+
+ function _getOptionString(filters, checkboxes) {
+ var args = Object.keys(filters).reduce(function(acc, key) {
+ if (filters[key].value) {
+ return acc + '&' + key + '=' + encodeURIComponent(filters[key].value);
+ }
+ return acc;
+ }, '');
+ if (checkboxes.presubmit != undefined && checkboxes.presubmit) {
+ args += '&showPresubmit='
+ }
+ if (checkboxes.postsubmit != undefined && checkboxes.postsubmit) {
+ args += '&showPostsubmit='
+ }
+ return args;
+ }
+
+ /**
+ * Create a search header element.
+ * @param label The header label.
+ * @param value The value to display next to the label.
+ * @param onRefreshCallback The function to call on refresh.
+ */
+ $.fn.createSearchHeader = function(label, value, onRefreshCallback) {
+ var self = $(this);
+ $.widget('custom.sizedAutocomplete', $.ui.autocomplete, {
+ _resizeMenu : function() {
+ this.menu.element.outerWidth($('.search-bar .filter-input').width());
+ }
+ });
+ var filters = {};
+ var checkboxes = {};
+ var expandOnRender = false;
+ var displayed = false;
+ return {
+ /**
+ * Add a filter to the display.
+ * @param displayName The input placeholder/label text.
+ * @param keyName The URL key to use for the filter options.
+ * @param options A dict of additional options (e.g. width, type).
+ * @param defaultValue A default filter value.
+ */
+ addFilter : function(displayName, keyName, options, defaultValue) {
+ if (displayed) return;
+ _addFilter(filters, displayName, keyName, options, defaultValue);
+ if (defaultValue) expandOnRender = true;
+ },
+ /**
+ * Enable run type checkboxes in the filter options.
+ *
+ * This will display two checkboxes for selecting pre-/postsubmit runs.
+ * @param showPresubmit True if presubmit runs are selected.
+ * @param showPostsubmit True if postsubmit runs are selected.
+ *
+ */
+ addRunTypeCheckboxes: function(showPresubmit, showPostsubmit) {
+ if (displayed) return;
+ checkboxes['presubmit'] = showPresubmit;
+ checkboxes['postsubmit'] = showPostsubmit;
+ if (!showPostsubmit || showPresubmit) {
+ expandOnRender = true;
+ }
+ },
+ /**
+ * Display the created search bar.
+ *
+ * This must be called after filters have been added. After displaying, no
+ * modifications to the filter options will take effect.
+ */
+ display : function() {
+ displayed = true;
+ _renderHeader(
+ self, label, value, filters, checkboxes, expandOnRender,
+ onRefreshCallback);
+ },
+ /**
+ * Get the URL arguments string for the current set of filters.
+ * @returns a URI-encoded component with the search bar keys and values.
+ */
+ args : function () {
+ return _getOptionString(filters, checkboxes);
+ }
+ }
+ }
+
+})(jQuery);
diff --git a/src/main/webapp/js/test_results.js b/src/main/webapp/js/test_results.js
new file mode 100644
index 0000000..8f923fb
--- /dev/null
+++ b/src/main/webapp/js/test_results.js
@@ -0,0 +1,272 @@
+/**
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may
+ * obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+(function ($, moment) {
+
+ /**
+ * Display the log links in a modal window.
+ * @param logList A list of [name, url] tuples representing log links.
+ */
+ function showLogs(container, logList) {
+ if (!logList || logList.length == 0) return;
+
+ var logCollection = $('<ul class="collection"></ul>');
+ var entries = logList.reduce(function(acc, entry) {
+ if (!entry || entry.length == 0) return acc;
+ var link = '<a href="' + entry[1] + '"';
+ link += 'class="collection-item">' + entry[0] + '</li>';
+ return acc + link;
+ }, '');
+ logCollection.html(entries);
+
+ if (container.find('#info-modal').length == 0) {
+ var modal = $('<div id="info-modal" class="modal"></div>');
+ var content = $('<div class="modal-content"></div>');
+ content.append('<h4>Logs</h4>');
+ content.append('<div class="info-container"></div>');
+ content.appendTo(modal);
+ modal.appendTo(container);
+ }
+ var infoContainer = $('#info-modal>.modal-content>.info-container');
+ infoContainer.empty();
+ logCollection.appendTo(infoContainer);
+ $('#info-modal').openModal();
+ }
+
+ /**
+ * Get the nickname for a test case result.
+ *
+ * Removes the result prefix and suffix, extracting only the result name.
+ *
+ * @param testCaseResult The string name of a VtsReportMessage.TestCaseResult.
+ * @returns the string nickname of the result.
+ */
+ function getNickname(testCaseResult) {
+ return testCaseResult
+ .replace('TEST_CASE_RESULT_', '')
+ .replace('_RESULT', '')
+ .trim().toLowerCase();
+ }
+
+ /**
+ * Display test data in the body beneath a test run's metadata.
+ * @param container The jquery object in which to insert the test metadata.
+ * @param data The json object containing the columns to display.
+ * @param lineHeight The height of each list element.
+ */
+ function displayTestDetails(container, data, lineHeight) {
+ var nCol = data.length;
+ var width = 12 / nCol;
+ test = container;
+ var maxLines = 0;
+ data.forEach(function (column) {
+ if (column.data == undefined || column.name == undefined) {
+ return;
+ }
+ var colContainer =
+ $('<div class="col s' + width + ' test-col"></div>');
+ var col = $('<div class="test-case-container"></div>');
+ colContainer.appendTo(container);
+ var count = column.data.length;
+ $('<h5>' + getNickname(column.name) + ' (' + count + ')' + '</h5>')
+ .appendTo(colContainer).css('text-transform', 'capitalize');
+ col.appendTo(colContainer);
+ var list = $('<ul></ul>').appendTo(col);
+ column.data.forEach(function (testCase) {
+ $('<li></li>')
+ .text(testCase)
+ .addClass('test-case')
+ .css('font-size', lineHeight - 2)
+ .css('line-height', lineHeight + 'px')
+ .appendTo(list);
+ });
+ if (count > maxLines) {
+ maxLines = count;
+ }
+ });
+ var containers = container.find('.test-case-container');
+ containers.height(maxLines * lineHeight);
+ }
+
+ /**
+ * Click handler for displaying test run details.
+ * @param e The click event.
+ */
+ function testRunClick(e) {
+ var header = $(this);
+ var icon = header.find('.material-icons.expand-arrow');
+ var container = header.parent().find('.test-results');
+ var test = header.attr('test');
+ var time = header.attr('time');
+ var url = '/api/test_run?test=' + test + '&timestamp=' + time;
+ if (header.parent().hasClass('active')) {
+ header.parent().removeClass('active');
+ header.removeClass('active');
+ icon.removeClass('rotate');
+ header.siblings('.collapsible-body').stop(true, false).slideUp({
+ duration: 100,
+ easing: "easeOutQuart",
+ queue: false,
+ complete: function() { header.css('height', ''); }
+ });
+ } else {
+ container.empty();
+ header.parent().addClass('active');
+ header.addClass('active');
+ header.addClass('disabled');
+ icon.addClass('rotate');
+ $.get(url).done(function(data) {
+ displayTestDetails(container, data, 16);
+ header.siblings('.collapsible-body').stop(true, false).slideDown({
+ duration: 100,
+ easing: "easeOutQuart",
+ queue: false,
+ complete: function() { header.css('height', ''); }
+ });
+ }).fail(function() {
+ icon.removeClass('rotate');
+ }).always(function() {
+ header.removeClass('disabled');
+ });
+ }
+ }
+
+ /**
+ * Append a clickable indicator link to the container.
+ * @param container The jquery object to append the indicator to.
+ * @param content The text to display in the indicator.
+ * @param classes Additional space-delimited classes to add to the indicator.
+ * @param click The click handler to assign to the indicator.
+ * @returns The jquery object for the indicator.
+ */
+ function createClickableIndicator(container, content, classes, click) {
+ var link = $('<a></a>');
+ link.addClass('indicator right center padded hoverable waves-effect');
+ link.addClass(classes)
+ link.append(content);
+ link.appendTo(container);
+ link.click(click);
+ return link;
+ }
+
+ function displayTestMetadata(container, metadataList, showTestNames=false) {
+ var popout = $('<ul></ul>');
+ popout.attr('data-collapsible', 'expandable');
+ popout.addClass('collapsible popout test-runs');
+ popout.appendTo(container);
+ popout.unbind();
+ metadataList.forEach(function (metadata) {
+ var li = $('<li class="test-run-container"></li>');
+ li.appendTo(popout);
+ var div = $('<div></div>');
+ var test = metadata.testRun.testName;
+ var startTime = metadata.testRun.startTimestamp;
+ var endTime = metadata.testRun.endTimestamp;
+ div.attr('test', test);
+ div.attr('time', startTime);
+ div.addClass('collapsible-header test-run');
+ div.appendTo(li);
+ div.unbind().click(testRunClick);
+ var span = $('<span></span>');
+ span.addClass('test-run-metadata');
+ span.appendTo(div);
+ span.click(function() { return false; });
+ if (showTestNames) {
+ $('<span class="test-run-label"></span>').text(test).appendTo(span);
+ span.append('<br>');
+ }
+ $('<b></b>').text(metadata.deviceInfo).appendTo(span);
+ span.append('<br>');
+ $('<b></b>').text('ABI: ')
+ .appendTo(span)
+ span.append(metadata.abiInfo).append('<br>');
+ $('<b></b>').text('VTS Build: ')
+ .appendTo(span)
+ span.append(metadata.testRun.testBuildId).append('<br>');
+ $('<b></b>').text('Host: ')
+ .appendTo(span)
+ span.append(metadata.testRun.hostName).append('<br>');
+ var timeString = (
+ moment().renderTime(startTime, false) + ' - ' +
+ moment().renderTime(endTime, true) + ' (' +
+ moment().renderDuration(endTime - startTime) + ')');
+ span.append(timeString);
+ var indicator = $('<span></span>');
+ var color = metadata.testRun.failCount > 0 ? 'red' : 'green';
+ indicator.addClass('indicator right center ' + color);
+ indicator.append(
+ metadata.testRun.passCount + '/' +
+ (metadata.testRun.passCount + metadata.testRun.failCount));
+ indicator.appendTo(div);
+ if (metadata.testRun.coveredLineCount != undefined &&
+ metadata.testRun.totalLineCount != undefined) {
+ var url = (
+ '/show_coverage?testName=' + test + '&startTime=' + startTime);
+ covered = metadata.testRun.coveredLineCount;
+ total = metadata.testRun.totalLineCount;
+ covPct = Math.round(covered / total * 1000) / 10;
+ var color = 'red';
+ if (covPct > 20 && covPct < 70) {
+ color = 'orange';
+ } else if (covPct >= 70) {
+ color = 'green';
+ }
+ var coverage = (
+ 'Coverage: ' + covered + '/' + total + ' (' + covPct + '%)');
+ createClickableIndicator(
+ div, coverage, color,
+ function () { window.location.href = url; return false; });
+ }
+ if (metadata.testRun.logLinks != undefined) {
+ createClickableIndicator(
+ div, 'Logs', 'grey lighten-1',
+ function () {
+ showLogs(popout, metadata.testRun.logLinks);
+ return false;
+ });
+ }
+ var expand = $('<i></i>');
+ expand.addClass('material-icons expand-arrow')
+ expand.text('expand_more');
+ expand.appendTo(div);
+ var body = $('<div></div>')
+ .addClass('collapsible-body test-results row grey lighten-4')
+ .appendTo(li);
+ if (metadata.testDetails != undefined) {
+ expand.addClass('rotate');
+ li.addClass('active');
+ div.addClass('active');
+ displayTestDetails(body, metadata.testDetails, 16);
+ div.siblings('.collapsible-body').stop(true, false).slideDown({
+ duration: 0,
+ queue: false,
+ complete: function() { div.css('height', ''); }
+ });
+ }
+ });
+ }
+
+ /**
+ * Display test metadata in a vertical popout.
+ * @param container The jquery object in which to insert the test metadata.
+ * @param metadataList The list of metadata objects to render on the display.
+ * @param showTestNames True to label each entry with the test module name.
+ */
+ $.fn.showTests = function(metadataList, showTestNames=false) {
+ displayTestMetadata($(this), metadataList, showTestNames);
+ }
+
+})(jQuery, moment);
diff --git a/src/main/webapp/js/time.js b/src/main/webapp/js/time.js
new file mode 100644
index 0000000..c5fbef6
--- /dev/null
+++ b/src/main/webapp/js/time.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may
+ * obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+(function (moment) {
+
+ /**
+ * Renders a timestamp in the user timezone.
+ * @param timestamp The long timestamp to render (in microseconds).
+ * @param showTimezone True if the timezone should be rendered, false otherwise.
+ * @returns the string-formatted version of the provided timestamp.
+ */
+ moment.prototype.renderTime = function (timestamp, showTimezone) {
+ var time = moment(timestamp / 1000);
+ var format = 'H:mm:ss';
+ if (!time.isSame(moment(), 'd')) {
+ format = 'M/D/YY ' + format;
+ }
+ if (!!showTimezone) {
+ format = format + 'ZZ';
+ }
+ return time.format(format);
+ }
+
+ /**
+ * Renders a duration in the user timezone.
+ * @param durationTimestamp The long duration to render (in microseconds).
+ * @returns the string-formatted duration of the provided duration timestamp.
+ */
+ moment.prototype.renderDuration = function (durationTimestamp) {
+ var fmt = 's[s]';
+ var duration = moment.utc(durationTimestamp / 1000);
+ if (duration.hours() > 0) {
+ fmt = 'H[h], m[m], ' + fmt;
+ } else if (duration.minutes() > 0) {
+ fmt = 'm[m], ' + fmt;
+ }
+ return duration.format(fmt);
+ }
+
+})(moment);