More improvements to HTTP baseline viewer (for GM results)
authorepoger@google.com <epoger@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81>
Wed, 2 Oct 2013 18:57:48 +0000 (18:57 +0000)
committerepoger@google.com <epoger@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81>
Wed, 2 Oct 2013 18:57:48 +0000 (18:57 +0000)
(SkipBuildbotRuns)

R=borenet@google.com, bsalomon@google.com

Review URL: https://codereview.chromium.org/25555003

git-svn-id: http://skia.googlecode.com/svn/trunk@11581 2bbb7eff-a529-9590-31e7-b0007b416f81

gm/rebaseline_server/results.py
gm/rebaseline_server/static/loader.js
gm/rebaseline_server/static/view.html

index 1336097f1f0390310d04cc8c3839e8a70267b287..6ac397ecebe20023ad0ff16e2127b21ce257efb8 100755 (executable)
@@ -124,6 +124,13 @@ class Results(object):
     """
     test_data = []
     category_dict = {}
+    Results._EnsureIncludedInCategoryDict(category_dict, 'resultType', [
+        gm_json.JSONKEY_ACTUALRESULTS_FAILED,
+        gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
+        gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
+        gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED,
+        ])
+
     for builder in sorted(actual_builder_dicts.keys()):
       actual_results_for_this_builder = (
           actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS])
@@ -205,17 +212,13 @@ class Results(object):
               "expectedHashDigest": str(expected_image[1]),
           }
           Results._AddToCategoryDict(category_dict, results_for_this_test)
-
-          # TODO(epoger): For now, don't include succeeded results in the raw
-          # data. There are so many of them that they make the client too slow.
-          if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED:
-            test_data.append(results_for_this_test)
+          test_data.append(results_for_this_test)
     return {"categories": category_dict, "testData": test_data}
 
   @staticmethod
   def _AddToCategoryDict(category_dict, test_results):
-    """Add test_results to the category dictionary we are building
-    (see documentation of self.GetAll() for the format of this dictionary).
+    """Add test_results to the category dictionary we are building.
+    (See documentation of self.GetAll() for the format of this dictionary.)
 
     params:
       category_dict: category dict-of-dicts to add to; modify this in-place
@@ -235,3 +238,22 @@ class Results(object):
       if not category_dict[category].get(category_value):
         category_dict[category][category_value] = 0
       category_dict[category][category_value] += 1
+
+  @staticmethod
+  def _EnsureIncludedInCategoryDict(category_dict,
+                                    category_name, category_values):
+    """Ensure that the category name/value pairs are included in category_dict,
+    even if there aren't any results with that name/value pair.
+    (See documentation of self.GetAll() for the format of this dictionary.)
+
+    params:
+      category_dict: category dict-of-dicts to modify
+      category_name: category name, as a string
+      category_values: list of values we want to make sure are represented
+                       for this category
+    """
+    if not category_dict.get(category_name):
+      category_dict[category_name] = {}
+    for category_value in category_values:
+      if not category_dict[category_name].get(category_value):
+        category_dict[category_name][category_value] = 0
index d08fdccbf64116d5f907d182103804470e3c8b55..8b5374a323320018e259d886f1280655842b16a9 100644 (file)
@@ -7,16 +7,99 @@ var Loader = angular.module(
     'Loader',
     []
 );
+
+// TODO(epoger): Combine ALL of our filtering operations (including
+// truncation) into this one filter, so that runs most efficiently?
+// (We would have to make sure truncation still took place after
+// sorting, though.)
+Loader.filter(
+  'removeHiddenItems',
+  function() {
+    return function(unfilteredItems, hiddenResultTypes, hiddenConfigs) {
+      var filteredItems = [];
+      for (var i = 0; i < unfilteredItems.length; i++) {
+       var item = unfilteredItems[i];
+       if ((hiddenResultTypes.indexOf(item.resultType) < 0) &&
+           (hiddenConfigs.indexOf(item.config) < 0)) {
+         filteredItems.push(item);
+       }
+      }
+      return filteredItems;
+    };
+  }
+);
+
 Loader.controller(
   'Loader.Controller',
-  function($scope, $http) {
+  function($scope, $http, $filter) {
     $http.get("/results/all").then(
       function(response) {
         $scope.categories = response.data.categories;
         $scope.testData = response.data.testData;
         $scope.sortColumn = 'test';
-       $scope.showResultsOfType = 'failed';
+
+        $scope.hiddenResultTypes = [
+          'failure-ignored', 'no-comparison', 'succeeded'];
+        $scope.hiddenConfigs = [];
+
+        $scope.updateResults();
       }
     );
+
+    $scope.isHiddenResultType = function(thisResultType) {
+      return ($scope.hiddenResultTypes.indexOf(thisResultType) >= 0);
+    }
+    $scope.toggleHiddenResultType = function(thisResultType) {
+      var i = $scope.hiddenResultTypes.indexOf(thisResultType);
+      if (i >= 0) {
+       $scope.hiddenResultTypes.splice(i, 1);
+      } else {
+       $scope.hiddenResultTypes.push(thisResultType);
+      }
+      $scope.areUpdatesPending = true;
+    }
+
+    // TODO(epoger): Rather than maintaining these as hard-coded
+    // variants of isHiddenResultType and toggleHiddenResultType, we
+    // should create general-purpose functions that can work with ANY
+    // category.
+    // But for now, I wanted to see this working. :-)
+    $scope.isHiddenConfig = function(thisConfig) {
+      return ($scope.hiddenConfigs.indexOf(thisConfig) >= 0);
+    }
+    $scope.toggleHiddenConfig = function(thisConfig) {
+      var i = $scope.hiddenConfigs.indexOf(thisConfig);
+      if (i >= 0) {
+       $scope.hiddenConfigs.splice(i, 1);
+      } else {
+       $scope.hiddenConfigs.push(thisConfig);
+      }
+      $scope.areUpdatesPending = true;
+    }
+
+    $scope.updateResults = function() {
+      $scope.displayLimit = $scope.displayLimitPending;
+      // TODO(epoger): Every time we apply a filter, AngularJS creates
+      // another copy of the array.  Is there a way we can filter out
+      // the items as they are displayed, rather than storing multiple
+      // array copies?  (For better performance.)
+      $scope.filteredTestData =
+         $filter("orderBy")(
+             $filter("removeHiddenItems")(
+                 $scope.testData,
+                 $scope.hiddenResultTypes,
+                 $scope.hiddenConfigs
+             ),
+             $scope.sortColumn);
+      $scope.limitedTestData = $filter("limitTo")(
+         $scope.filteredTestData, $scope.displayLimit);
+      $scope.imageSize = $scope.imageSizePending;
+      $scope.areUpdatesPending = false;
+    }
+
+    $scope.sortResultsBy = function(sortColumn) {
+      $scope.sortColumn = sortColumn;
+      $scope.updateResults();
+    }
   }
 );
index 5913e5c2caa6f5ded4b5788d9c3199670839270d..cd3ab1589584ef54b4c1fc1714502fa2d0d0995e 100644 (file)
   <!-- TODO(epoger): Add some indication of how old the
   expected/actual data is -->
 
- Settings:
-  <ul>
-    <!-- TODO(epoger): Now that we get multiple result types in a single
-    fetch, modify the UI: add a column showing resultType, and allow
-    the user to sort/filter on that column just like all the
-    others. -->
-    <li>show results of type
-      <select ng-model="showResultsOfType"
-              ng-init="showResultsOfType='failed'">
-        <option ng-repeat="(resultType, count) in categories['resultType']"
-                value="{{resultType}}">
-          {{resultType}} ({{count}})
-        </option>
-      </select>
-      <!--
-        TODO(epoger): See results.py: for now, I have disabled
-        returning succeeded tests as part of the JSON, because it
-        makes the returned JSON too big (and slows down the client).
+  <em ng-hide="categories">
+    Loading data, please wait...
+  </em>
 
-        Also, we should use some sort of lazy-loading technique
-        (e.g. http://www.appelsiini.net/projects/lazyload ), so that
-        the images are only loaded as they become viewable...
-        that will help with long lists like resultType='no-comparison'.
-      -->
-      <br>
-      TODO(epoger): 'no-comparison' will probably take forever;
-      see HTML source for details
-      <br>
-      TODO(epoger): 'succeeded' will not show any results;
-      see HTML source for details
-    </li>
-    <li>image size
-      <input type="text" ng-model="imageSizePending"
-             ng-init="imageSizePending=100; imageSize=100"
-             maxlength="4"/>
-      <button ng:click="imageSize=imageSizePending">apply</button>
-    </li>
-  </ul>
+  <div ng-hide="!categories">
+  <table border="1">
+    <tr>
+      <th colspan="2">
+        Filters
+      </th>
+      <th>
+        Settings
+      </th>
+    </tr>
+    <tr valign="top">
+      <td>
+        resultType<br>
+        <label ng-repeat="(resultType, count) in categories['resultType']">
+          <input type="checkbox"
+                 name="resultTypes"
+                 value="{{resultType}}"
+                 ng-checked="!isHiddenResultType(resultType)"
+                 ng-click="toggleHiddenResultType(resultType)">
+          {{resultType}} ({{count}})<br>
+        </label>
+      </td>
+      <td>
+        config<br>
+        <label ng-repeat="(config, count) in categories['config']">
+          <input type="checkbox"
+                 name="configs"
+                 value="{{config}}"
+                 ng-checked="!isHiddenConfig(config)"
+                 ng-click="toggleHiddenConfig(config)">
+          {{config}} ({{count}})<br>
+        </label>
+      </td>
+      <td><table>
+        <tr><td>
+          Image size
+          <input type="text" ng-model="imageSizePending"
+                 ng-init="imageSizePending=100"
+                 ng-change="areUpdatesPending = true"
+                 maxlength="4"/>
+        </td></tr>
+        <tr><td>
+          Max records to display
+          <input type="text" ng-model="displayLimitPending"
+                 ng-init="displayLimitPending=50"
+                 ng-change="areUpdatesPending = true"
+                 maxlength="4"/>
+        </td></tr>
+        <tr><td>
+          <button style="font-size:30px"
+                  ng-click="updateResults()"
+                  ng-disabled="!areUpdatesPending">
+            Update Results
+          </button>
+        </td></tr>
+      </tr></table></td>
+    </tr>
+  </table>
 
     <p>
-      Click on the column header radio buttons to re-sort by that column...<br>
-      <!-- TODO(epoger): Show some sort of "loading" message, instead of
-           an empty table, while the data is loading.  Otherwise, if there are
-           a lot of failures and it takes a long time to load them, the user
-           might think there are NO failures and leave the page! -->
+      TODO(epoger): Add ability to filter builder and test names
+      (using a free-form text field, with partial string match)
+      <br>
+      TODO(epoger): Add more columns, such as pixel diffs, notes/bugs,
+      ignoreFailure boolean
+      <br>
+      TODO(epoger): Improve the column sorting, as per
+      <a href="http://jsfiddle.net/vojtajina/js64b/14/">
+        http://jsfiddle.net/vojtajina/js64b/14/
+      </a>
+      <br>
+      TODO(epoger): Right now, if you change which column is used to
+      sort the data, the column widths may fluctuate based on the
+      longest string <i>currently visible</i> within the top {{displayLimit}}
+      results.  Can we fix the column widths to be wide enough to hold
+      any result, even the currently hidden results?
+      <p>
+      Found {{filteredTestData.length}} matches, and displaying the first
+      {{displayLimit}}: <br>
+      <!-- TODO(epoger): If (displayLimit <= filteredTestData.length),
+           modify this message to indicate that all results are shown. -->
+      (click on the column header radio buttons to re-sort by that column)
+      <br>
       <table border="1">
         <tr>
-          <th><input ng-model="sortColumn" name="sortColumnRadio" type="radio" value="builder">Builder</input></th>
-          <th><input ng-model="sortColumn" name="sortColumnRadio" type="radio" value="test">Test</input></th>
-          <th><input ng-model="sortColumn" name="sortColumnRadio" type="radio" value="config">Config</input></th>
-          <th><input ng-model="sortColumn" name="sortColumnRadio" type="radio" value="expectedHashDigest">Expected Image</input></th>
-          <th><input ng-model="sortColumn" name="sortColumnRadio" type="radio" value="actualHashDigest">Actual Image</input></th>
-          <!-- TODO(epoger): Add more columns, such as...
-               pixel diff
-               notes/bugs
-               ignoreFailure boolean
-          -->
+          <th ng-repeat="categoryName in ['resultType', 'builder', 'test', 'config']">
+            <input type="radio"
+                   name="sortColumnRadio"
+                   value="{{categoryName}}"
+                   ng-checked="(sortColumn == categoryName)"
+                   ng-click="sortResultsBy(categoryName)">
+            {{categoryName}}
+          </th>
+          <th>
+            <input type="radio"
+                   name="sortColumnRadio"
+                   value="expectedHashDigest"
+                   ng-checked="(sortColumn == 'expectedHashDigest')"
+                   ng-click="sortResultsBy('expectedHashDigest')">
+            expected image
+          </th>
+          <th>
+            <input type="radio"
+                   name="sortColumnRadio"
+                   value="actualHashDigest"
+                   ng-checked="(sortColumn == 'actualHashDigest')"
+                   ng-click="sortResultsBy('actualHashDigest')">
+            actual image
+          </th>
         </tr>
-        <!-- TODO(epoger): improve the column sorting, as per
-             http://jsfiddle.net/vojtajina/js64b/14/ -->
-        <tr ng-repeat="result in testData | filter: { resultType: showResultsOfType } | orderBy: sortColumn">
+        <tr ng-repeat="result in limitedTestData">
+          <td>{{result.resultType}}</td>
           <td>{{result.builder}}</td>
           <td>{{result.test}}</td>
           <td>{{result.config}}</td>
         </tr>
       </table>
   </div>
+  </div>
 
   <!-- TODO(epoger): Can we get the base URLs (commondatastorage and
        issues list) from