diff options
author | Ryan Campbell <ryanjcampbell@google.com> | 2017-06-19 11:17:10 -0700 |
---|---|---|
committer | Ryan Campbell <ryanjcampbell@google.com> | 2017-06-19 12:26:07 -0700 |
commit | 67c9a5397bd972a173cdf08aaccba44678f3aea0 (patch) | |
tree | e02d3fa1d9c91d8cfaaef276c2b3ffb4c064a4b5 /src/main/webapp | |
parent | 7054de7189ba17d6516861e437b8f50fc837191d (diff) | |
download | dashboard-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')
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 + '×tamp=' + 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); |