Merge "Merge favorites and preferences UI."
diff --git a/web/dashboard/src/main/java/com/android/vts/api/UserFavoriteRestServlet.java b/web/dashboard/src/main/java/com/android/vts/api/UserFavoriteRestServlet.java
new file mode 100644
index 0000000..f863051
--- /dev/null
+++ b/web/dashboard/src/main/java/com/android/vts/api/UserFavoriteRestServlet.java
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+
+package com.android.vts.api;
+
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.UserFavoriteEntity;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Transaction;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.api.users.User;
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Servlet for handling requests to add or remove subscriptions. */
+public class UserFavoriteRestServlet extends HttpServlet {
+ protected static final Logger logger =
+ Logger.getLogger(UserFavoriteRestServlet.class.getName());
+
+ /**
+ * Add a test to the user's favorites.
+ */
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ UserService userService = UserServiceFactory.getUserService();
+ User currentUser = userService.getCurrentUser();
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+ // Retrieve the added tests from the request.
+ String test = request.getPathInfo();
+ if (test == null) {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+ if (test.startsWith("/")) {
+ test = test.substring(1);
+ }
+ Key addedTestKey = KeyFactory.createKey(TestEntity.KIND, test);
+ // Filter the tests that exist from the set of tests to add
+ try {
+ datastore.get(addedTestKey);
+ } catch (EntityNotFoundException e) {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ Filter userFilter =
+ new FilterPredicate(UserFavoriteEntity.USER, FilterOperator.EQUAL, currentUser);
+ Filter testFilter = new FilterPredicate(
+ UserFavoriteEntity.TEST_KEY, FilterOperator.EQUAL, addedTestKey);
+ Query q = new Query(UserFavoriteEntity.KIND)
+ .setFilter(CompositeFilterOperator.and(userFilter, testFilter))
+ .setKeysOnly();
+
+ Key favoriteKey = null;
+
+ Transaction txn = datastore.beginTransaction();
+ try {
+ for (Entity e : datastore.prepare(q).asIterable()) {
+ favoriteKey = e.getKey();
+ break;
+ }
+ if (favoriteKey == null) {
+ UserFavoriteEntity favorite = new UserFavoriteEntity(currentUser, addedTestKey);
+ Entity entity = favorite.toEntity();
+ datastore.put(entity);
+ favoriteKey = entity.getKey();
+ }
+ txn.commit();
+ } finally {
+ if (txn.isActive()) {
+ logger.log(Level.WARNING,
+ "Transaction rollback forced for favorite creation: " + test);
+ txn.rollback();
+ }
+ }
+
+ response.setContentType("application/json");
+ PrintWriter writer = response.getWriter();
+ JsonObject json = new JsonObject();
+ json.add("key", new JsonPrimitive(KeyFactory.keyToString(favoriteKey)));
+ writer.print(new Gson().toJson(json));
+ writer.flush();
+ response.setStatus(HttpServletResponse.SC_OK);
+ }
+
+ /**
+ * Remove a test from the user's favorites.
+ */
+ @Override
+ public void doDelete(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ String stringKey = request.getPathInfo();
+ if (stringKey == null) {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+ if (stringKey.startsWith("/")) {
+ stringKey = stringKey.substring(1);
+ }
+ datastore.delete(KeyFactory.stringToKey(stringKey));
+ response.setStatus(HttpServletResponse.SC_OK);
+ }
+}
diff --git a/web/dashboard/src/main/java/com/android/vts/servlet/DashboardMainServlet.java b/web/dashboard/src/main/java/com/android/vts/servlet/DashboardMainServlet.java
index a1c7132..0c2a86a 100644
--- a/web/dashboard/src/main/java/com/android/vts/servlet/DashboardMainServlet.java
+++ b/web/dashboard/src/main/java/com/android/vts/servlet/DashboardMainServlet.java
@@ -22,16 +22,16 @@
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.PropertyProjection;
import com.google.appengine.api.datastore.Query;
-import com.google.appengine.api.datastore.Query.CompositeFilter;
-import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
import com.google.appengine.api.datastore.Query.Filter;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.FilterPredicate;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
+import com.google.gson.Gson;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
@@ -51,8 +51,6 @@
private static final String DASHBOARD_FAVORITES_LINK = "/";
private static final String ALL_HEADER = "All Tests";
private static final String FAVORITES_HEADER = "Favorites";
- private static final String NO_FAVORITES_ERROR =
- "No subscribed tests. Click the edit button to add to favorites.";
private static final String NO_TESTS_ERROR = "No test results available.";
private static final String FAVORITES_BUTTON = "Show Favorites";
private static final String ALL_BUTTON = "Show All";
@@ -71,16 +69,19 @@
/** Helper class for displaying test entries on the main dashboard. */
public class TestDisplay implements Comparable<TestDisplay> {
private final Key testKey;
+ private final int passCount;
private final int failCount;
/**
* Test display constructor.
*
* @param testKey The key of the test.
+ * @param passCount The number of tests passing.
* @param failCount The number of tests failing.
*/
- public TestDisplay(Key testKey, int failCount) {
+ public TestDisplay(Key testKey, int passCount, int failCount) {
this.testKey = testKey;
+ this.passCount = passCount;
this.failCount = failCount;
}
@@ -94,6 +95,15 @@
}
/**
+ * Get the number of passing test cases.
+ *
+ * @return The number of passing test cases.
+ */
+ public int getPassCount() {
+ return this.passCount;
+ }
+
+ /**
* Get the number of failing test cases.
*
* @return The number of failing test cases.
@@ -117,8 +127,10 @@
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
List<TestDisplay> displayedTests = new ArrayList<>();
+ List<String> allTests = new ArrayList<>();
- Map<Key, Integer> failCountMap = new HashMap<>(); // map from table name to fail count
+ Map<Key, TestDisplay> testMap = new HashMap<>(); // map from table key to TestDisplay
+ Map<String, String> subscriptionMap = new HashMap<>();
boolean showAll = request.getParameter("showAll") != null;
String header;
@@ -128,44 +140,46 @@
String error = null;
Query q = new Query(TestEntity.KIND)
+ .addProjection(new PropertyProjection(TestEntity.PASS_COUNT, Long.class))
.addProjection(new PropertyProjection(TestEntity.FAIL_COUNT, Long.class));
for (Entity test : datastore.prepare(q).asIterable()) {
TestEntity testEntity = TestEntity.fromEntity(test);
if (test != null) {
- failCountMap.put(test.getKey(), testEntity.failCount);
+ TestDisplay display =
+ new TestDisplay(test.getKey(), testEntity.passCount, testEntity.failCount);
+ testMap.put(test.getKey(), display);
+ allTests.add(test.getKey().getName());
}
}
+ if (testMap.size() == 0) {
+ error = NO_TESTS_ERROR;
+ }
+
if (showAll) {
- for (Key testKey : failCountMap.keySet()) {
- TestDisplay test = new TestDisplay(testKey, failCountMap.get(testKey));
- displayedTests.add(test);
- }
- if (displayedTests.size() == 0) {
- error = NO_TESTS_ERROR;
+ for (Key testKey : testMap.keySet()) {
+ displayedTests.add(testMap.get(testKey));
}
header = ALL_HEADER;
buttonLabel = FAVORITES_BUTTON;
buttonIcon = UP_ARROW;
buttonLink = DASHBOARD_FAVORITES_LINK;
} else {
- if (failCountMap.size() > 0) {
+ if (testMap.size() > 0) {
Filter userFilter = new FilterPredicate(
UserFavoriteEntity.USER, FilterOperator.EQUAL, currentUser);
- Filter propertyFilter = new FilterPredicate(
- UserFavoriteEntity.TEST_KEY, FilterOperator.IN, failCountMap.keySet());
- CompositeFilter filter = CompositeFilterOperator.and(userFilter, propertyFilter);
- q = new Query(UserFavoriteEntity.KIND).setFilter(filter);
+ q = new Query(UserFavoriteEntity.KIND).setFilter(userFilter);
for (Entity favorite : datastore.prepare(q).asIterable()) {
Key testKey = (Key) favorite.getProperty(UserFavoriteEntity.TEST_KEY);
- TestDisplay test = new TestDisplay(testKey, failCountMap.get(testKey));
- displayedTests.add(test);
+ if (!testMap.containsKey(testKey)) {
+ continue;
+ }
+ displayedTests.add(testMap.get(testKey));
+ subscriptionMap.put(
+ testKey.getName(), KeyFactory.keyToString(favorite.getKey()));
}
}
- if (displayedTests.size() == 0) {
- error = NO_FAVORITES_ERROR;
- }
header = FAVORITES_HEADER;
buttonLabel = ALL_BUTTON;
buttonIcon = DOWN_ARROW;
@@ -174,6 +188,8 @@
Collections.sort(displayedTests);
response.setStatus(HttpServletResponse.SC_OK);
+ request.setAttribute("allTestsJson", new Gson().toJson(allTests));
+ request.setAttribute("subscriptionMapJson", new Gson().toJson(subscriptionMap));
request.setAttribute("testNames", displayedTests);
request.setAttribute("headerLabel", header);
request.setAttribute("showAll", showAll);
diff --git a/web/dashboard/src/main/java/com/android/vts/servlet/ShowPreferencesServlet.java b/web/dashboard/src/main/java/com/android/vts/servlet/ShowPreferencesServlet.java
deleted file mode 100644
index fc1db79..0000000
--- a/web/dashboard/src/main/java/com/android/vts/servlet/ShowPreferencesServlet.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.vts.servlet;
-
-import com.android.vts.entity.TestEntity;
-import com.android.vts.entity.UserFavoriteEntity;
-import com.google.appengine.api.datastore.DatastoreService;
-import com.google.appengine.api.datastore.DatastoreServiceFactory;
-import com.google.appengine.api.datastore.Entity;
-import com.google.appengine.api.datastore.Key;
-import com.google.appengine.api.datastore.KeyFactory;
-import com.google.appengine.api.datastore.Query;
-import com.google.appengine.api.datastore.Query.Filter;
-import com.google.appengine.api.datastore.Query.FilterOperator;
-import com.google.appengine.api.datastore.Query.FilterPredicate;
-import com.google.appengine.api.users.User;
-import com.google.appengine.api.users.UserService;
-import com.google.appengine.api.users.UserServiceFactory;
-import com.google.gson.Gson;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.logging.Level;
-import javax.servlet.RequestDispatcher;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.apache.commons.lang.StringUtils;
-
-/** Represents the servlet that is invoked on loading the preferences page to manage favorites. */
-public class ShowPreferencesServlet extends BaseServlet {
- private static final String PREFERENCES_JSP = "WEB-INF/jsp/show_preferences.jsp";
- private static final String DASHBOARD_MAIN_LINK = "/";
-
- /** Helper class for displaying test subscriptions. */
- public class Subscription {
- private final String testName;
- private final String key;
-
- /**
- * Test display constructor.
- *
- * @param testName The name of the test.
- * @param key The websafe string serialization of the subscription key.
- */
- public Subscription(String testName, String key) {
- this.testName = testName;
- this.key = key;
- }
-
- /**
- * Get the name of the test.
- *
- * @return The name of the test.
- */
- public String getTestName() {
- return this.testName;
- }
-
- /**
- * Get the subscription key.
- *
- * @return The subscription key.
- */
- public String getKey() {
- return this.key;
- }
- }
-
- @Override
- public List<String[]> getNavbarLinks(HttpServletRequest request) {
- List<String[]> links = new ArrayList<>();
- Page root = Page.HOME;
- String[] rootEntry = new String[] {root.getUrl(), root.getName()};
- links.add(rootEntry);
-
- Page prefs = Page.PREFERENCES;
- String[] prefsEntry = new String[] {CURRENT_PAGE, prefs.getName()};
- links.add(prefsEntry);
- return links;
- }
-
- @Override
- public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
- throws IOException {
- // Get the user's information
- UserService userService = UserServiceFactory.getUserService();
- User currentUser = userService.getCurrentUser();
- RequestDispatcher dispatcher = null;
- DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
-
- List<Subscription> subscriptions = new ArrayList<>();
-
- // Map from TestEntity key to the UserFavoriteEntity object
- Map<Key, Entity> subscriptionEntityMap = new HashMap<>();
-
- // Map from test name to the subscription object
- Map<String, Subscription> subscriptionMap = new HashMap<>();
-
- // Query for the favorites entities matching the current user
- Filter propertyFilter =
- new FilterPredicate(UserFavoriteEntity.USER, FilterOperator.EQUAL, currentUser);
- Query q = new Query(UserFavoriteEntity.KIND).setFilter(propertyFilter);
- for (Entity favorite : datastore.prepare(q).asIterable()) {
- UserFavoriteEntity favoriteEntity = UserFavoriteEntity.fromEntity(favorite);
- if (favoriteEntity == null) {
- continue;
- }
- subscriptionEntityMap.put(favoriteEntity.testKey, favorite);
- }
- if (subscriptionEntityMap.size() > 0) {
- // Query for the tests specified by the user favorite entities
- propertyFilter = new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.IN,
- subscriptionEntityMap.keySet());
- q = new Query(TestEntity.KIND).setFilter(propertyFilter).setKeysOnly();
- for (Entity test : datastore.prepare(q).asIterable()) {
- String testName = test.getKey().getName();
- Entity subscriptionEntity = subscriptionEntityMap.get(test.getKey());
- Subscription sub = new Subscription(
- testName, KeyFactory.keyToString(subscriptionEntity.getKey()));
- subscriptions.add(sub);
- subscriptionMap.put(testName, sub);
- }
- }
- List<String> allTests = new ArrayList<>();
- for (Entity result : datastore.prepare(new Query(TestEntity.KIND)).asIterable()) {
- allTests.add(result.getKey().getName());
- }
-
- request.setAttribute("allTestsJson", new Gson().toJson(allTests));
- request.setAttribute("subscriptions", subscriptions);
- request.setAttribute("subscriptionMapJson", new Gson().toJson(subscriptionMap));
- request.setAttribute("subscriptionsJson", new Gson().toJson(subscriptions));
-
- dispatcher = request.getRequestDispatcher(PREFERENCES_JSP);
- try {
- dispatcher.forward(request, response);
- } catch (ServletException e) {
- logger.log(Level.SEVERE, "Servlet Exception caught : ", e);
- }
- }
-
- @Override
- public void doPost(HttpServletRequest request, HttpServletResponse response)
- throws IOException {
- UserService userService = UserServiceFactory.getUserService();
- User currentUser = userService.getCurrentUser();
- DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
-
- // Retrieve the added tests from the request.
- String addedTestsString = request.getParameter("addedTests");
- List<Key> addedTests = new ArrayList<>();
- if (!StringUtils.isBlank(addedTestsString)) {
- for (String test : addedTestsString.trim().split(",")) {
- addedTests.add(KeyFactory.createKey(TestEntity.KIND, test));
- }
- }
- if (addedTests.size() > 0) {
- // Filter the tests that exist from the set of tests to add
- Filter propertyFilter = new FilterPredicate(
- Entity.KEY_RESERVED_PROPERTY, FilterOperator.IN, addedTests);
- Query q = new Query(TestEntity.KIND).setFilter(propertyFilter).setKeysOnly();
- List<Entity> newSubscriptions = new ArrayList<>();
-
- // Create subscription entities
- for (Entity test : datastore.prepare(q).asIterable()) {
- UserFavoriteEntity favorite = new UserFavoriteEntity(currentUser, test.getKey());
- newSubscriptions.add(favorite.toEntity());
- }
- datastore.put(newSubscriptions);
- }
-
- // Retrieve the removed tests from the request.
- String removedSubscriptionString = request.getParameter("removedKeys");
- if (!StringUtils.isBlank(removedSubscriptionString)) {
- for (String stringKey : removedSubscriptionString.trim().split(",")) {
- try {
- datastore.delete(KeyFactory.stringToKey(stringKey));
- } catch (IllegalArgumentException e) {
- continue;
- }
- }
- }
- response.sendRedirect(DASHBOARD_MAIN_LINK);
- }
-}
diff --git a/web/dashboard/src/main/webapp/WEB-INF/datastore-indexes.xml b/web/dashboard/src/main/webapp/WEB-INF/datastore-indexes.xml
index dfade2f..d3f02f3 100644
--- a/web/dashboard/src/main/webapp/WEB-INF/datastore-indexes.xml
+++ b/web/dashboard/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -14,6 +14,11 @@
<datastore-indexes autoGenerate="true">
+ <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"/>
diff --git a/web/dashboard/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp b/web/dashboard/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp
index dc6698e..3b72110 100644
--- a/web/dashboard/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp
+++ b/web/dashboard/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp
@@ -19,51 +19,159 @@
<html>
<link rel='stylesheet' href='/css/dashboard_main.css'>
- <%@ include file="header.jsp" %>
+ <%@ 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'>
- <div class='row' id='options'>
- <c:choose>
- <c:when test="${not empty error}">
- <div id="error-container" class="row card">
- <div class="col s12 center-align">
- <h5>${error}</h5>
+ <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:when>
- <c:otherwise>
+ </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'>
- <a href='/show_table?testName=${test.name}'>
- <div class='col s12 card hoverable option valign-wrapper waves-effect'>
- <span class='entry valign'>${test.name}
- <c:if test='${test.failCount > 0}'>
- <span class='indicator red center'>
- ${test.failCount}
- </span>
- </c:if>
- </span>
- </div>
- </a>
+ <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>
- </c:otherwise>
- </c:choose>
- </div>
- <div class='row center-align'>
+ </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>
- </div>
- <c:if test='${not showAll}'>
- <div id='edit-button-wrapper' class='fixed-action-btn'>
- <a href='/show_preferences' id='edit-button' class='btn-floating btn-large red waves-effect'>
- <i class='large material-icons'>mode_edit</i>
- </a>
- </div>
</c:if>
- <%@ include file="footer.jsp" %>
+ <%@ include file='footer.jsp' %>
</body>
</html>
diff --git a/web/dashboard/src/main/webapp/WEB-INF/jsp/show_preferences.jsp b/web/dashboard/src/main/webapp/WEB-INF/jsp/show_preferences.jsp
deleted file mode 100644
index ec6ddee..0000000
--- a/web/dashboard/src/main/webapp/WEB-INF/jsp/show_preferences.jsp
+++ /dev/null
@@ -1,153 +0,0 @@
-<%--
- ~ 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_preferences.css'>
- <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.min.js'></script>
- <body>
- <script>
- var subscriptions = ${subscriptionsJson};
- var subscriptionMap = ${subscriptionMapJson};
- var allTests = ${allTestsJson};
- var testSet = new Set(allTests);
- var addedSet = new Set();
- var removedKeySet = new Set();
-
- var addFunction = function() {
- var test = $('#input-box').val();
- if (testSet.has(test) && !subscriptionMap[test] && !addedSet.has(test)) {
- var icon = $('<i></i>').addClass('material-icons')
- .html('clear');
- var clear = $('<a></a>').addClass('btn-flat clear-button')
- .append(icon)
- .click(clearFunction);
- var span = $('<span></span>').addClass('entry valign')
- .html(test);
- var div = $('<div></div>').addClass('col s12 card option valign-wrapper')
- .append(span).append(clear)
- .prependTo('#options')
- .hide()
- .slideDown(150);
- if (!subscriptionMap[test]) {
- addedSet.add(test);
- } else {
- removedKeySet.delete(subscriptionMap[test].key);
- }
- $('#input-box').val('').focusout();
- if (!addedSet.size && !removedKeySet.size) {
- $('#save-button-wrapper').slideUp(50);
- } else {
- $('#save-button-wrapper').slideDown(50);
- }
- }
- }
-
- var clearFunction = function() {
- var div = $(this).parent();
- div.slideUp(150, function() {
- div.remove();
- });
- var test = div.find('span').text();
- if (subscriptionMap[test]) {
- removedKeySet.add(subscriptionMap[test].key);
- } else {
- addedSet.delete(test);
- }
- if (!addedSet.size && !removedKeySet.size) {
- $('#save-button-wrapper').slideUp(50);
- } else {
- $('#save-button-wrapper').slideDown(50);
- }
- }
-
- var submitForm = function() {
- var added = Array.from(addedSet).join(',');
- var removed = Array.from(removedKeySet).join(',');
- $('#prefs-form>input[name="addedTests"]').val(added);
- $('#prefs-form>input[name="removedKeys"]').val(removed);
- $('#prefs-form').submit();
- }
-
- $.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(clearFunction);
- $('#add-button').click(addFunction);
- $('#save-button').click(submitForm);
- $('#save-button-wrapper').hide();
- });
- </script>
- <div class='container'>
- <div class='row'>
- <h3 class='col s12 header'>Favorites</h3>
- <p class='col s12 caption'>Add or remove tests from favorites to customize
- the dashboard. Tests in your favorites will send you email notifications
- to let you know when test cases change status.
- </p>
- </div>
- <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>
- <div class='row' id='options'>
- <c:forEach items='${subscriptions}' var='subscription' varStatus='loop'>
- <div class='col s12 card option valign-wrapper'>
- <span class='entry valign' index=${loop.index} key=${subscription.key}>${subscription.testName}</span>
- <a class='btn-flat clear-button'>
- <i class='material-icons'>clear</i>
- </a>
- </div>
- </c:forEach>
- </div>
- </div>
- <form id='prefs-form' style='visibility:hidden' action='/show_preferences' method='post'>
- <input name='addedTests' type='text'>
- <input name='removedKeys' type='text'>
- </form>
- <div id='save-button-wrapper' class='fixed-action-btn'>
- <a id='save-button' class='btn-floating btn-large red waves-effect'>
- <i class='large material-icons'>done</i>
- </a>
- </div>
- <%@ include file="footer.jsp" %>
- </body>
-</html>
diff --git a/web/dashboard/src/main/webapp/WEB-INF/web.xml b/web/dashboard/src/main/webapp/WEB-INF/web.xml
index 4bd1475..9efc059 100644
--- a/web/dashboard/src/main/webapp/WEB-INF/web.xml
+++ b/web/dashboard/src/main/webapp/WEB-INF/web.xml
@@ -46,11 +46,6 @@
</servlet>
<servlet>
- <servlet-name>show_preferences</servlet-name>
- <servlet-class>com.android.vts.servlet.ShowPreferencesServlet</servlet-class>
-</servlet>
-
-<servlet>
<servlet-name>datastore</servlet-name>
<servlet-class>com.android.vts.api.DatastoreRestServlet</servlet-class>
</servlet>
@@ -61,6 +56,11 @@
</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>
@@ -81,11 +81,6 @@
</servlet-mapping>
<servlet-mapping>
- <servlet-name>show_preferences</servlet-name>
- <url-pattern>/show_preferences/*</url-pattern>
-</servlet-mapping>
-
-<servlet-mapping>
<servlet-name>show_tree</servlet-name>
<url-pattern>/show_tree/*</url-pattern>
</servlet-mapping>
@@ -126,6 +121,11 @@
</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>
diff --git a/web/dashboard/src/main/webapp/css/dashboard_main.css b/web/dashboard/src/main/webapp/css/dashboard_main.css
index 9dfcf38..f675e00 100644
--- a/web/dashboard/src/main/webapp/css/dashboard_main.css
+++ b/web/dashboard/src/main/webapp/css/dashboard_main.css
@@ -18,20 +18,25 @@
right: 25px;
}
-th {
- background: #551A8B; /* Darken header a bit */
- font-weight: bold;
- color: white;
+.input-field {
+ margin-bottom: 50px;
}
-td {
- background: #912CEE;
- text-align: center;
+#add-button-wrapper {
+ margin-top: 10px;
+ height: 61px;
}
-.row .col.s12.card.option {
- padding: 10px 30px;
- margin: 8px;
+.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 {
@@ -40,7 +45,7 @@
}
.entry {
- font-size: 21px;
+ font-size: 20px;
font-weight: 300;
position: relative;
}
@@ -54,11 +59,7 @@
right: 0;
min-width: 40px;
border-radius: 10px;
- margin-top: 6px;
-}
-
-#options {
- min-height: 80%;
+ margin-top: 5px;
}
#show-button {
@@ -81,4 +82,26 @@
margin-bottom: 30px;
display: block;
content: " ";
-}
\ No newline at end of file
+}
+
+.ui-menu {
+ z-index: 100;
+}
+
+.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/web/dashboard/src/main/webapp/css/show_preferences.css b/web/dashboard/src/main/webapp/css/show_preferences.css
deleted file mode 100644
index 9731037..0000000
--- a/web/dashboard/src/main/webapp/css/show_preferences.css
+++ /dev/null
@@ -1,75 +0,0 @@
-/* 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.
-*/
-
-.card.option {
- padding: 10px 30px;
- margin: 8px;
- color: #039be5;
-}
-
-#add-button-wrapper {
- margin-top: 10px;
- height: 61px;
-}
-
-#save-button-wrapper {
- bottom: 25px;
- right: 25px;
-}
-
-.clear-button {
- position: absolute;
- right: 0;
-}
-
-.header {
- cursor: default;
- color: #ee6e73;
- margin-bottom: 0;
-}
-
-.caption {
- cursor: default;
- font-size: 20px;
- font-weight: 300;
- margin-bottom: 30px;
-}
-
-.entry {
- font-size: 21px;
- font-weight: 300;
-}
-
-.ui-menu {
- z-index: 100;
-}
-
-.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;
-}
\ No newline at end of file