rebaseline_server: allow JSON to control column filtering

Makes the rebaseline_server client more generic, allowing the server to tweak display properties by writing directives into the JSON file.

Adds two new fields to the rebaseline_server JSON file (and thus increments VALUE__HEADER__SCHEMA_VERSION):
1. KEY__ROOT__EXTRACOLUMNORDER: order in which the client should display columns
2. KEY__EXTRACOLUMNHEADERS__USE_FREEFORM_FILTER: whether a column should be filtered using a freeform text field or checkboxes

BUG=skia:2230
NOTRY=True
R=rmistry@google.com

Author: epoger@google.com

Review URL: https://codereview.chromium.org/376623002
This commit is contained in:
epoger 2014-07-09 06:19:20 -07:00 committed by Commit bot
parent 874a62acef
commit b4edbffd7c
11 changed files with 306 additions and 201 deletions

View File

@ -15,6 +15,7 @@ KEY__EXTRACOLUMNHEADERS__HEADER_TEXT = 'headerText'
KEY__EXTRACOLUMNHEADERS__HEADER_URL = 'headerUrl'
KEY__EXTRACOLUMNHEADERS__IS_FILTERABLE = 'isFilterable'
KEY__EXTRACOLUMNHEADERS__IS_SORTABLE = 'isSortable'
KEY__EXTRACOLUMNHEADERS__USE_FREEFORM_FILTER = 'useFreeformFilter'
KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS = 'valuesAndCounts'
@ -23,7 +24,7 @@ class ColumnHeaderFactory(object):
def __init__(self, header_text, header_url=None,
is_filterable=True, is_sortable=True,
include_values_and_counts=True):
use_freeform_filter=False):
"""
Args:
header_text: string; text the client should display within column header.
@ -32,15 +33,16 @@ class ColumnHeaderFactory(object):
is_filterable: boolean; whether client should allow filtering on this
column.
is_sortable: boolean; whether client should allow sorting on this column.
include_values_and_counts: boolean; whether the set of values found
within this column, and their counts, should be available for the
client to display.
use_freeform_filter: boolean; *recommendation* to the client indicating
whether to allow freeform text matching, as opposed to listing all
values alongside checkboxes. If is_filterable==false, this is
meaningless.
"""
self._header_text = header_text
self._header_url = header_url
self._is_filterable = is_filterable
self._is_sortable = is_sortable
self._include_values_and_counts = include_values_and_counts
self._use_freeform_filter = use_freeform_filter
def create_as_dict(self, values_and_counts_dict=None):
"""Creates the header for this column, in dictionary form.
@ -58,10 +60,11 @@ class ColumnHeaderFactory(object):
KEY__EXTRACOLUMNHEADERS__HEADER_TEXT: self._header_text,
KEY__EXTRACOLUMNHEADERS__IS_FILTERABLE: self._is_filterable,
KEY__EXTRACOLUMNHEADERS__IS_SORTABLE: self._is_sortable,
KEY__EXTRACOLUMNHEADERS__USE_FREEFORM_FILTER: self._use_freeform_filter,
}
if self._header_url:
asdict[KEY__EXTRACOLUMNHEADERS__HEADER_URL] = self._header_url
if self._include_values_and_counts and values_and_counts_dict:
if values_and_counts_dict:
asdict[KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS] = sorted(
values_and_counts_dict.items())
return asdict

View File

@ -12,16 +12,18 @@ Repackage expected/actual GM results as needed by our HTML rebaseline viewer.
# System-level imports
import argparse
import fnmatch
import json
import logging
import os
import re
import sys
import time
# Must fix up PYTHONPATH before importing from within Skia
# pylint: disable=W0611
import fix_pythonpath
# pylint: enable=W0611
# Imports from within Skia
import fix_pythonpath # must do this first
from pyutils import url_utils
import column
import gm_json
import imagediffdb
import imagepair
@ -33,6 +35,17 @@ EXPECTATION_FIELDS_PASSED_THRU_VERBATIM = [
results.KEY__EXPECTATIONS__IGNOREFAILURE,
results.KEY__EXPECTATIONS__REVIEWED,
]
FREEFORM_COLUMN_IDS = [
results.KEY__EXTRACOLUMNS__BUILDER,
results.KEY__EXTRACOLUMNS__TEST,
]
ORDERED_COLUMN_IDS = [
results.KEY__EXTRACOLUMNS__RESULT_TYPE,
results.KEY__EXTRACOLUMNS__BUILDER,
results.KEY__EXTRACOLUMNS__TEST,
results.KEY__EXTRACOLUMNS__CONFIG,
]
TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm')
DEFAULT_IGNORE_FAILURES_FILE = 'ignored-tests.txt'
@ -171,7 +184,7 @@ class ExpectationComparisons(results.BaseComparisons):
if not os.path.isdir(root):
raise IOError('no directory found at path %s' % root)
actual_builders_written = []
for dirpath, dirnames, filenames in os.walk(root):
for dirpath, _, filenames in os.walk(root):
for matching_filename in fnmatch.filter(filenames, pattern):
builder = os.path.basename(dirpath)
per_builder_dict = meta_dict.get(builder)
@ -211,6 +224,15 @@ class ExpectationComparisons(results.BaseComparisons):
descriptions=IMAGEPAIR_SET_DESCRIPTIONS,
diff_base_url=self._diff_base_url)
# Override settings for columns that should be filtered using freeform text.
for column_id in FREEFORM_COLUMN_IDS:
factory = column.ColumnHeaderFactory(
header_text=column_id, use_freeform_filter=True)
all_image_pairs.set_column_header_factory(
column_id=column_id, column_header_factory=factory)
failing_image_pairs.set_column_header_factory(
column_id=column_id, column_header_factory=factory)
all_image_pairs.ensure_extra_column_values_in_summary(
column_id=results.KEY__EXTRACOLUMNS__RESULT_TYPE, values=[
results.KEY__RESULT_TYPE__FAILED,
@ -339,9 +361,12 @@ class ExpectationComparisons(results.BaseComparisons):
except Exception:
logging.exception('got exception while creating new ImagePair')
# pylint: disable=W0201
self._results = {
results.KEY__HEADER__RESULTS_ALL: all_image_pairs.as_dict(),
results.KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(),
results.KEY__HEADER__RESULTS_ALL: all_image_pairs.as_dict(
column_ids_in_order=ORDERED_COLUMN_IDS),
results.KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(
column_ids_in_order=ORDERED_COLUMN_IDS),
}

View File

@ -19,6 +19,7 @@ import imagediffdb
# Keys used within dictionary representation of ImagePairSet.
# NOTE: Keep these in sync with static/constants.js
KEY__ROOT__EXTRACOLUMNHEADERS = 'extraColumnHeaders'
KEY__ROOT__EXTRACOLUMNORDER = 'extraColumnOrder'
KEY__ROOT__HEADER = 'header'
KEY__ROOT__IMAGEPAIRS = 'imagePairs'
KEY__ROOT__IMAGESETS = 'imageSets'
@ -135,15 +136,35 @@ class ImagePairSet(object):
values_for_column)
return asdict
def as_dict(self):
def as_dict(self, column_ids_in_order=None):
"""Returns a dictionary describing this package of ImagePairs.
Uses the KEY__* constants as keys.
Params:
column_ids_in_order: A list of all extracolumn IDs in the desired display
order. If unspecified, they will be displayed in alphabetical order.
If specified, this list must contain all the extracolumn IDs!
(It may contain extra column IDs; they will be ignored.)
"""
all_column_ids = set(self._extra_column_tallies.keys())
if column_ids_in_order == None:
column_ids_in_order = sorted(all_column_ids)
else:
# Make sure the caller listed all column IDs, and throw away any extras.
specified_column_ids = set(column_ids_in_order)
forgotten_column_ids = all_column_ids - specified_column_ids
assert not forgotten_column_ids, (
'column_ids_in_order %s missing these column_ids: %s' % (
column_ids_in_order, forgotten_column_ids))
column_ids_in_order = [c for c in column_ids_in_order
if c in all_column_ids]
key_description = KEY__IMAGESETS__FIELD__DESCRIPTION
key_base_url = KEY__IMAGESETS__FIELD__BASE_URL
return {
KEY__ROOT__EXTRACOLUMNHEADERS: self._column_headers_as_dict(),
KEY__ROOT__EXTRACOLUMNORDER: column_ids_in_order,
KEY__ROOT__IMAGEPAIRS: self._image_pair_dicts,
KEY__ROOT__IMAGESETS: {
KEY__IMAGESETS__SET__IMAGE_A: {

View File

@ -89,6 +89,7 @@ class ImagePairSetTest(unittest.TestCase):
'headerText': 'builder',
'isFilterable': True,
'isSortable': True,
'useFreeformFilter': False,
'valuesAndCounts': [('MyBuilder', 3)],
},
'test': {
@ -96,8 +97,13 @@ class ImagePairSetTest(unittest.TestCase):
'headerUrl': 'http://learn/about/gm/tests',
'isFilterable': True,
'isSortable': False,
'useFreeformFilter': False,
'valuesAndCounts': [('test1', 1),
('test2', 1),
('test3', 1)],
},
},
'extraColumnOrder': ['builder', 'test'],
'imagePairs': [
IMAGEPAIR_1_AS_DICT,
IMAGEPAIR_2_AS_DICT,
@ -136,8 +142,7 @@ class ImagePairSetTest(unittest.TestCase):
header_text='which GM test',
header_url='http://learn/about/gm/tests',
is_filterable=True,
is_sortable=False,
include_values_and_counts=False))
is_sortable=False))
self.assertEqual(image_pair_set.as_dict(), expected_imageset_dict)
def test_mismatched_base_url(self):
@ -153,6 +158,23 @@ class ImagePairSetTest(unittest.TestCase):
MockImagePair(base_url=BASE_URL_2,
dict_to_return=IMAGEPAIR_3_AS_DICT))
def test_missing_column_ids(self):
"""Confirms that passing truncated column_ids_in_order to as_dict()
will cause an exception."""
image_pair_set = imagepairset.ImagePairSet(
diff_base_url=DIFF_BASE_URL)
image_pair_set.add_image_pair(
MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_1_AS_DICT))
image_pair_set.add_image_pair(
MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_2_AS_DICT))
# Call as_dict() with default or reasonable column_ids_in_order.
image_pair_set.as_dict()
image_pair_set.as_dict(column_ids_in_order=['test', 'builder'])
image_pair_set.as_dict(column_ids_in_order=['test', 'builder', 'extra'])
# Call as_dict() with not enough column_ids.
with self.assertRaises(Exception):
image_pair_set.as_dict(column_ids_in_order=['builder'])
class MockImagePair(object):
"""Mock ImagePair object, which will return canned results."""

View File

@ -14,14 +14,18 @@ import fnmatch
import os
import re
# Must fix up PYTHONPATH before importing from within Skia
# pylint: disable=W0611
import fix_pythonpath
# pylint: enable=W0611
# Imports from within Skia
import fix_pythonpath # must do this first
import gm_json
import imagepairset
# Keys used to link an image to a particular GM test.
# NOTE: Keep these in sync with static/constants.js
VALUE__HEADER__SCHEMA_VERSION = 3
VALUE__HEADER__SCHEMA_VERSION = 4
KEY__EXPECTATIONS__BUGS = gm_json.JSONKEY_EXPECTEDRESULTS_BUGS
KEY__EXPECTATIONS__IGNOREFAILURE = gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE
KEY__EXPECTATIONS__REVIEWED = gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED
@ -201,7 +205,7 @@ class BaseComparisons(object):
if not os.path.isdir(root):
raise IOError('no directory found at path %s' % root)
meta_dict = {}
for dirpath, dirnames, filenames in os.walk(root):
for dirpath, _, filenames in os.walk(root):
for matching_filename in fnmatch.filter(filenames, pattern):
builder = os.path.basename(dirpath)
if self._ignore_builder(builder):
@ -228,7 +232,7 @@ class BaseComparisons(object):
if not os.path.isdir(root):
raise IOError('no directory found at path %s' % root)
meta_dict = {}
for abs_dirpath, dirnames, filenames in os.walk(root):
for abs_dirpath, _, filenames in os.walk(root):
rel_dirpath = os.path.relpath(abs_dirpath, root)
for matching_filename in fnmatch.filter(filenames, pattern):
abs_path = os.path.join(abs_dirpath, matching_filename)
@ -293,7 +297,7 @@ class BaseComparisons(object):
If this would result in any repeated keys, it will raise an Exception.
"""
output_dict = {}
for key, subdict in input_dict.iteritems():
for subdict in input_dict.values():
for subdict_key, subdict_value in subdict.iteritems():
if subdict_key in output_dict:
raise Exception('duplicate key %s in combine_subdicts' % subdict_key)

View File

@ -13,6 +13,7 @@ module.constant('constants', (function() {
KEY__EXTRACOLUMNHEADERS__HEADER_URL: 'headerUrl',
KEY__EXTRACOLUMNHEADERS__IS_FILTERABLE: 'isFilterable',
KEY__EXTRACOLUMNHEADERS__IS_SORTABLE: 'isSortable',
KEY__EXTRACOLUMNHEADERS__USE_FREEFORM_FILTER: 'useFreeformFilter',
KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS: 'valuesAndCounts',
// NOTE: Keep these in sync with ../imagediffdb.py
@ -31,6 +32,7 @@ module.constant('constants', (function() {
// NOTE: Keep these in sync with ../imagepairset.py
KEY__ROOT__EXTRACOLUMNHEADERS: 'extraColumnHeaders',
KEY__ROOT__EXTRACOLUMNORDER: 'extraColumnOrder',
KEY__ROOT__HEADER: 'header',
KEY__ROOT__IMAGEPAIRS: 'imagePairs',
KEY__ROOT__IMAGESETS: 'imageSets',
@ -62,7 +64,7 @@ module.constant('constants', (function() {
KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: 'timeNextUpdateAvailable',
KEY__HEADER__TIME_UPDATED: 'timeUpdated',
KEY__HEADER__TYPE: 'type',
VALUE__HEADER__SCHEMA_VERSION: 3,
VALUE__HEADER__SCHEMA_VERSION: 4,
//
KEY__RESULT_TYPE__FAILED: 'failed',
KEY__RESULT_TYPE__FAILUREIGNORED: 'failure-ignored',

View File

@ -30,24 +30,29 @@ Loader.directive(
Loader.filter(
'removeHiddenImagePairs',
function(constants) {
return function(unfilteredImagePairs, showingColumnValues,
builderSubstring, testSubstring, viewingTab) {
return function(unfilteredImagePairs, filterableColumnNames, showingColumnValues,
viewingTab) {
var filteredImagePairs = [];
for (var i = 0; i < unfilteredImagePairs.length; i++) {
var imagePair = unfilteredImagePairs[i];
var extraColumnValues = imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS];
// For performance, we examine the "set" objects directly rather
// than calling $scope.isValueInSet().
// Besides, I don't think we have access to $scope in here...
if (showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE]
[extraColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE]] &&
showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG]
[extraColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG]] &&
!(-1 == extraColumnValues[constants.KEY__EXTRACOLUMNS__BUILDER]
.indexOf(builderSubstring)) &&
!(-1 == extraColumnValues[constants.KEY__EXTRACOLUMNS__TEST]
.indexOf(testSubstring)) &&
(viewingTab == imagePair.tab)) {
var allColumnValuesAreVisible = true;
// Loop over all columns, and if any of them contain values not found in
// showingColumnValues[columnName], don't include this imagePair.
//
// We use this same filtering mechanism regardless of whether each column
// has USE_FREEFORM_FILTER set or not; if that flag is set, then we will
// have already used the freeform text entry block to populate
// showingColumnValues[columnName].
for (var j = 0; j < filterableColumnNames.length; j++) {
var columnName = filterableColumnNames[j];
var columnValue = extraColumnValues[columnName];
if (!showingColumnValues[columnName][columnValue]) {
allColumnValuesAreVisible = false;
break;
}
}
if (allColumnValuesAreVisible && (viewingTab == imagePair.tab)) {
filteredImagePairs.push(imagePair);
}
}
@ -159,6 +164,7 @@ Loader.controller(
$scope.header = dataHeader;
$scope.extraColumnHeaders = data[constants.KEY__ROOT__EXTRACOLUMNHEADERS];
$scope.orderedColumnNames = data[constants.KEY__ROOT__EXTRACOLUMNORDER];
$scope.imagePairs = data[constants.KEY__ROOT__IMAGEPAIRS];
$scope.imageSets = data[constants.KEY__ROOT__IMAGESETS];
$scope.sortColumnSubdict = constants.KEY__IMAGEPAIRS__DIFFERENCES;
@ -200,41 +206,69 @@ Loader.controller(
// Arrays within which the user can toggle individual elements.
$scope.selectedImagePairs = [];
// Set up filters.
//
// filterableColumnNames is a list of all column names we can filter on.
// allColumnValues[columnName] is a list of all known values
// for this column.
// for a given column.
// showingColumnValues[columnName] is a set indicating which values
// in this column would cause us to show a row, rather than hiding it.
// in a given column would cause us to show a row, rather than hiding it.
//
// columnStringMatch[columnName] is a string used as a pattern to generate
// showingColumnValues[columnName] for columns we filter using free-form text.
// It is ignored for any columns with USE_FREEFORM_FILTER == false.
$scope.filterableColumnNames = [];
$scope.allColumnValues = {};
$scope.showingColumnValues = {};
$scope.columnStringMatch = {};
// set allColumnValues/showingColumnValues for RESULT_TYPE;
angular.forEach(
Object.keys($scope.extraColumnHeaders),
function(columnName) {
var columnHeader = $scope.extraColumnHeaders[columnName];
if (columnHeader[constants.KEY__EXTRACOLUMNHEADERS__IS_FILTERABLE]) {
$scope.filterableColumnNames.push(columnName);
$scope.allColumnValues[columnName] = $scope.columnSliceOf2DArray(
columnHeader[constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS], 0);
$scope.showingColumnValues[columnName] = {};
$scope.toggleValuesInSet($scope.allColumnValues[columnName],
$scope.showingColumnValues[columnName]);
$scope.columnStringMatch[columnName] = "";
}
}
);
// TODO(epoger): Special handling for RESULT_TYPE column:
// by default, show only KEY__RESULT_TYPE__FAILED results
$scope.allColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE] =
$scope.columnSliceOf2DArray(
$scope.extraColumnHeaders[constants.KEY__EXTRACOLUMNS__RESULT_TYPE]
[constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS],
0);
$scope.showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE] = {};
$scope.showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE][
constants.KEY__RESULT_TYPE__FAILED] = true;
// set allColumnValues/showingColumnValues for CONFIG;
// by default, show results for all configs
$scope.allColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG] =
$scope.columnSliceOf2DArray(
$scope.extraColumnHeaders[constants.KEY__EXTRACOLUMNS__CONFIG]
[constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS],
0);
$scope.showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG] = {};
$scope.toggleValuesInSet($scope.allColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG],
$scope.showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG]);
// Associative array of partial string matches per category.
// TODO(epoger): Rename as columnValueMatch to be more consistent
// with allColumnValues/showingColumnValues ?
$scope.categoryValueMatch = {};
$scope.categoryValueMatch[constants.KEY__EXTRACOLUMNS__BUILDER] = "";
$scope.categoryValueMatch[constants.KEY__EXTRACOLUMNS__TEST] = "";
// Set up mapping for URL parameters.
// parameter name -> copier object to load/save parameter value
$scope.queryParameters.map = {
'resultsToLoad': $scope.queryParameters.copiers.simple,
'displayLimitPending': $scope.queryParameters.copiers.simple,
'showThumbnailsPending': $scope.queryParameters.copiers.simple,
'mergeIdenticalRowsPending': $scope.queryParameters.copiers.simple,
'imageSizePending': $scope.queryParameters.copiers.simple,
'sortColumnSubdict': $scope.queryParameters.copiers.simple,
'sortColumnKey': $scope.queryParameters.copiers.simple,
};
// Some parameters are handled differently based on whether they USE_FREEFORM_FILTER.
angular.forEach(
$scope.filterableColumnNames,
function(columnName) {
if ($scope.extraColumnHeaders[columnName]
[constants.KEY__EXTRACOLUMNHEADERS__USE_FREEFORM_FILTER]) {
$scope.queryParameters.map[columnName] =
$scope.queryParameters.copiers.columnStringMatch;
} else {
$scope.queryParameters.map[columnName] =
$scope.queryParameters.copiers.showingColumnValuesSet;
}
}
);
// If any defaults were overridden in the URL, get them now.
$scope.queryParameters.load();
@ -391,15 +425,15 @@ Loader.controller(
}
},
'categoryValueMatch': {
'columnStringMatch': {
'load': function(nameValuePairs, name) {
var value = nameValuePairs[name];
if (value) {
$scope.categoryValueMatch[name] = value;
$scope.columnStringMatch[name] = value;
}
},
'save': function(nameValuePairs, name) {
nameValuePairs[name] = $scope.categoryValueMatch[name];
nameValuePairs[name] = $scope.columnStringMatch[name];
}
},
@ -419,25 +453,6 @@ Loader.controller(
};
// parameter name -> copier objects to load/save parameter value
$scope.queryParameters.map = {
'resultsToLoad': $scope.queryParameters.copiers.simple,
'displayLimitPending': $scope.queryParameters.copiers.simple,
'showThumbnailsPending': $scope.queryParameters.copiers.simple,
'mergeIdenticalRowsPending': $scope.queryParameters.copiers.simple,
'imageSizePending': $scope.queryParameters.copiers.simple,
'sortColumnSubdict': $scope.queryParameters.copiers.simple,
'sortColumnKey': $scope.queryParameters.copiers.simple,
};
$scope.queryParameters.map[constants.KEY__EXTRACOLUMNS__RESULT_TYPE] =
$scope.queryParameters.copiers.showingColumnValuesSet;
$scope.queryParameters.map[constants.KEY__EXTRACOLUMNS__BUILDER] =
$scope.queryParameters.copiers.categoryValueMatch;
$scope.queryParameters.map[constants.KEY__EXTRACOLUMNS__TEST] =
$scope.queryParameters.copiers.categoryValueMatch;
$scope.queryParameters.map[constants.KEY__EXTRACOLUMNS__CONFIG] =
$scope.queryParameters.copiers.showingColumnValuesSet;
// Loads all parameters into $scope from the URL query string;
// any which are not found within the URL will keep their current value.
$scope.queryParameters.load = function() {
@ -550,6 +565,30 @@ Loader.controller(
$log.debug("renderStartTime: " + $scope.renderStartTime);
$scope.displayLimit = $scope.displayLimitPending;
$scope.mergeIdenticalRows = $scope.mergeIdenticalRowsPending;
// For each USE_FREEFORM_FILTER column, populate showingColumnValues.
// This is more efficient than applying the freeform filter within the
// tight loop in removeHiddenImagePairs.
angular.forEach(
$scope.filterableColumnNames,
function(columnName) {
var columnHeader = $scope.extraColumnHeaders[columnName];
if (columnHeader[constants.KEY__EXTRACOLUMNHEADERS__USE_FREEFORM_FILTER]) {
var columnStringMatch = $scope.columnStringMatch[columnName];
var showingColumnValues = {};
angular.forEach(
$scope.allColumnValues[columnName],
function(columnValue) {
if (-1 != columnValue.indexOf(columnStringMatch)) {
showingColumnValues[columnValue] = true;
}
}
);
$scope.showingColumnValues[columnName] = showingColumnValues;
}
}
);
// TODO(epoger): Every time we apply a filter, AngularJS creates
// another copy of the array. Is there a way we can filter out
// the imagePairs as they are displayed, rather than storing multiple
@ -569,9 +608,8 @@ Loader.controller(
$filter("orderBy")(
$filter("removeHiddenImagePairs")(
$scope.imagePairs,
$scope.filterableColumnNames,
$scope.showingColumnValues,
$scope.categoryValueMatch[constants.KEY__EXTRACOLUMNS__BUILDER],
$scope.categoryValueMatch[constants.KEY__EXTRACOLUMNS__TEST],
$scope.viewingTab
),
[$scope.getSortColumnValue, $scope.getSecondOrderSortValue],
@ -652,36 +690,40 @@ Loader.controller(
}
/**
* Set $scope.categoryValueMatch[name] = value, and update results.
* Set $scope.columnStringMatch[name] = value, and update results.
*
* @param name
* @param value
*/
$scope.setCategoryValueMatch = function(name, value) {
$scope.categoryValueMatch[name] = value;
$scope.setColumnStringMatch = function(name, value) {
$scope.columnStringMatch[name] = value;
$scope.updateResults();
}
/**
* Update $scope.showingColumnValues[columnName] so that ONLY entries with
* this columnValue are showing, and update the visible results.
* Update $scope.showingColumnValues[columnName] and $scope.columnStringMatch[columnName]
* so that ONLY entries with this columnValue are showing, and update the visible results.
* (We update both of those, so we cover both freeform and checkbox filtered columns.)
*
* @param columnName
* @param columnValue
*/
$scope.showOnlyColumnValue = function(columnName, columnValue) {
$scope.columnStringMatch[columnName] = columnValue;
$scope.showingColumnValues[columnName] = {};
$scope.toggleValueInSet(columnValue, $scope.showingColumnValues[columnName]);
$scope.updateResults();
}
/**
* Update $scope.showingColumnValues[columnName] so that ALL entries are
* showing, and update the visible results.
* Update $scope.showingColumnValues[columnName] and $scope.columnStringMatch[columnName]
* so that ALL entries are showing, and update the visible results.
* (We update both of those, so we cover both freeform and checkbox filtered columns.)
*
* @param columnName
*/
$scope.showAllColumnValues = function(columnName) {
$scope.columnStringMatch[columnName] = "";
$scope.showingColumnValues[columnName] = {};
$scope.toggleValuesInSet($scope.allColumnValues[columnName],
$scope.showingColumnValues[columnName]);

View File

@ -74,62 +74,53 @@
</th>
</tr>
<tr valign="top">
<td>
resultType<br>
<label ng-repeat="valueAndCount in extraColumnHeaders[constants.KEY__EXTRACOLUMNS__RESULT_TYPE][constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS]">
<input type="checkbox"
name="resultTypes"
value="{{valueAndCount[0]}}"
ng-checked="isValueInSet(valueAndCount[0], showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE])"
ng-click="toggleValueInSet(valueAndCount[0], showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE]); setUpdatesPending(true)">
{{valueAndCount[0]}} ({{valueAndCount[1]}})<br>
</label>
<button ng-click="showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE] = {}; toggleValuesInSet(allColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE], showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE]); updateResults()"
ng-disabled="!readyToDisplay || allColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE].length == setSize(showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE])">
all
</button>
<button ng-click="showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE] = {}; updateResults()"
ng-disabled="!readyToDisplay || 0 == setSize(showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE])">
none
</button>
<button ng-click="toggleValuesInSet(allColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE], showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE]); updateResults()">
toggle
</button>
</td>
<td ng-repeat="category in [constants.KEY__EXTRACOLUMNS__BUILDER, constants.KEY__EXTRACOLUMNS__TEST]">
{{category}}
<br>
<input type="text"
ng-model="categoryValueMatch[category]"
ng-change="setUpdatesPending(true)"/>
<br>
<button ng-click="setCategoryValueMatch(category, '')"
ng-disabled="('' == categoryValueMatch[category])">
clear (show all)
</button>
</td>
<td>
config<br>
<label ng-repeat="valueAndCount in extraColumnHeaders[constants.KEY__EXTRACOLUMNS__CONFIG][constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS]">
<input type="checkbox"
name="configs"
value="{{valueAndCount[0]}}"
ng-checked="isValueInSet(valueAndCount[0], showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG])"
ng-click="toggleValueInSet(valueAndCount[0], showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG]); setUpdatesPending(true)">
{{valueAndCount[0]}} ({{valueAndCount[1]}})<br>
</label>
<button ng-click="showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG] = {}; toggleValuesInSet(allColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG], showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG]); updateResults()"
ng-disabled="!readyToDisplay || allColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG].length == setSize(showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG])">
all
</button>
<button ng-click="showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG] = {}; updateResults()"
ng-disabled="!readyToDisplay || 0 == setSize(showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG])">
none
</button>
<button ng-click="toggleValuesInSet(allColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG], showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG]); updateResults()">
toggle
</button>
<!-- filters -->
<td ng-repeat="columnName in orderedColumnNames">
<!-- Only display filterable columns here... -->
<div ng-if="extraColumnHeaders[columnName][constants.KEY__EXTRACOLUMNHEADERS__IS_FILTERABLE]">
{{extraColumnHeaders[columnName][constants.KEY__EXTRACOLUMNHEADERS__HEADER_TEXT]}}<br>
<!-- If we filter this column using free-form text match... -->
<div ng-if="extraColumnHeaders[columnName][constants.KEY__EXTRACOLUMNHEADERS__USE_FREEFORM_FILTER]">
<input type="text"
ng-model="columnStringMatch[columnName]"
ng-change="setUpdatesPending(true)"/>
<br>
<button ng-click="setColumnStringMatch(columnName, '')"
ng-disabled="('' == columnStringMatch[columnName])">
clear (show all)
</button>
</div>
<!-- If we filter this column using checkboxes... -->
<div ng-if="!extraColumnHeaders[columnName][constants.KEY__EXTRACOLUMNHEADERS__USE_FREEFORM_FILTER]">
<label ng-repeat="valueAndCount in extraColumnHeaders[columnName][constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS]">
<input type="checkbox"
name="resultTypes"
value="{{valueAndCount[0]}}"
ng-checked="isValueInSet(valueAndCount[0], showingColumnValues[columnName])"
ng-click="toggleValueInSet(valueAndCount[0], showingColumnValues[columnName]); setUpdatesPending(true)">
{{valueAndCount[0]}} ({{valueAndCount[1]}})<br>
</label>
<button ng-click="showingColumnValues[columnName] = {}; toggleValuesInSet(allColumnValues[columnName], showingColumnValues[columnName]); updateResults()"
ng-disabled="!readyToDisplay || allColumnValues[columnName].length == setSize(showingColumnValues[columnName])">
all
</button>
<button ng-click="showingColumnValues[columnName] = {}; updateResults()"
ng-disabled="!readyToDisplay || 0 == setSize(showingColumnValues[columnName])">
none
</button>
<button ng-click="toggleValuesInSet(allColumnValues[columnName], showingColumnValues[columnName]); updateResults()">
toggle
</button>
</div>
</div>
</td>
<!-- settings -->
<td><table>
<tr><td>
<input type="checkbox" ng-model="showThumbnailsPending"
@ -248,13 +239,13 @@
<table border="1" ng-app="diff_viewer"> <!-- results -->
<tr>
<!-- Most column headers are displayed in a common fashion... -->
<th ng-repeat="categoryName in [constants.KEY__EXTRACOLUMNS__RESULT_TYPE, constants.KEY__EXTRACOLUMNS__BUILDER, constants.KEY__EXTRACOLUMNS__TEST, constants.KEY__EXTRACOLUMNS__CONFIG]">
<th ng-repeat="columnName in orderedColumnNames">
<input type="radio"
name="sortColumnRadio"
value="{{categoryName}}"
ng-checked="(sortColumnKey == categoryName)"
ng-click="sortResultsBy(constants.KEY__IMAGEPAIRS__EXTRACOLUMNS, categoryName)">
{{categoryName}}
value="{{columnName}}"
ng-checked="(sortColumnKey == columnName)"
ng-click="sortResultsBy(constants.KEY__IMAGEPAIRS__EXTRACOLUMNS, columnName)">
{{extraColumnHeaders[columnName][constants.KEY__EXTRACOLUMNHEADERS__HEADER_TEXT]}}
</th>
<!-- ... but there are a few columns where we display things differently. -->
<th>
@ -311,63 +302,28 @@
<tr ng-repeat="imagePair in limitedImagePairs" valign="top"
ng-class-odd="'results-odd'" ng-class-even="'results-even'"
results-updated-callback-directive>
<td>
{{imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][constants.KEY__EXTRACOLUMNS__RESULT_TYPE]}}
<td ng-repeat="columnName in orderedColumnNames">
{{imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][columnName]}}
<br>
<button class="show-only-button"
ng-show="viewingTab == defaultTab"
ng-disabled="1 == setSize(showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE])"
ng-click="showOnlyColumnValue(constants.KEY__EXTRACOLUMNS__RESULT_TYPE, imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][constants.KEY__EXTRACOLUMNS__RESULT_TYPE])"
title="show only results of type {{imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][constants.KEY__EXTRACOLUMNS__RESULT_TYPE]}}">
ng-disabled="1 == setSize(showingColumnValues[columnName])"
ng-click="showOnlyColumnValue(columnName, imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][columnName])"
title="show only results of {{extraColumnHeaders[columnName][constants.KEY__EXTRACOLUMNHEADERS__HEADER_TEXT]}} {{imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][columnName]}}">
show only
</button>
<br>
<button class="show-all-button"
ng-show="viewingTab == defaultTab"
ng-disabled="allColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE].length == setSize(showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE])"
ng-click="showAllColumnValues(constants.KEY__EXTRACOLUMNS__RESULT_TYPE)"
title="show results of all types">
show all
</button>
</td>
<td ng-repeat="categoryName in [constants.KEY__EXTRACOLUMNS__BUILDER, constants.KEY__EXTRACOLUMNS__TEST]">
{{imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][categoryName]}}
<br>
<button class="show-only-button"
ng-show="viewingTab == defaultTab"
ng-disabled="imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][categoryName] == categoryValueMatch[categoryName]"
ng-click="setCategoryValueMatch(categoryName, imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][categoryName])"
title="show only results of {{categoryName}} {{imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][categoryName]}}">
show only
</button>
<br>
<button class="show-all-button"
ng-show="viewingTab == defaultTab"
ng-disabled="'' == categoryValueMatch[categoryName]"
ng-click="setCategoryValueMatch(categoryName, '')"
title="show results of all {{categoryName}}s">
show all
</button>
</td>
<td>
{{imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][constants.KEY__EXTRACOLUMNS__CONFIG]}}
<br>
<button class="show-only-button"
ng-show="viewingTab == defaultTab"
ng-disabled="1 == setSize(showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG])"
ng-click="showOnlyColumnValue(constants.KEY__EXTRACOLUMNS__CONFIG, imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][constants.KEY__EXTRACOLUMNS__CONFIG])"
title="show only results of config {{imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS][constants.KEY__EXTRACOLUMNS__CONFIG]}}">
show only
</button>
<br>
<button class="show-all-button"
ng-show="viewingTab == defaultTab"
ng-disabled="allColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG].length == setSize(showingColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG])"
ng-click="showAllColumnValues(constants.KEY__EXTRACOLUMNS__CONFIG)"
title="show results of all configs">
ng-disabled="allColumnValues[columnName].length == setSize(showingColumnValues[columnName])"
ng-click="showAllColumnValues(columnName)"
title="show results of all {{extraColumnHeaders[columnName][constants.KEY__EXTRACOLUMNHEADERS__HEADER_TEXT]}}s">
show all
</button>
</td>
<!-- bugs -->
<td>
<a ng-repeat="bug in imagePair[constants.KEY__IMAGEPAIRS__EXPECTATIONS][constants.KEY__EXPECTATIONS__BUGS]"
href="https://code.google.com/p/skia/issues/detail?id={{bug}}"

View File

@ -4,6 +4,7 @@
"headerText": "builder",
"isFilterable": true,
"isSortable": true,
"useFreeformFilter": false,
"valuesAndCounts": [
[
"Test-Android-GalaxyNexus-SGX540-Arm7-Release",
@ -23,6 +24,7 @@
"headerText": "config",
"isFilterable": true,
"isSortable": true,
"useFreeformFilter": false,
"valuesAndCounts": [
[
"TODO",
@ -34,6 +36,7 @@
"headerText": "resultType",
"isFilterable": true,
"isSortable": true,
"useFreeformFilter": false,
"valuesAndCounts": [
[
"failed",
@ -53,6 +56,7 @@
"headerText": "test",
"isFilterable": true,
"isSortable": true,
"useFreeformFilter": false,
"valuesAndCounts": [
[
"3x3bitmaprect",
@ -93,11 +97,17 @@
]
}
},
"extraColumnOrder": [
"builder",
"config",
"resultType",
"test"
],
"header": {
"dataHash": "-5829724510169924592",
"isEditable": false,
"isExported": true,
"schemaVersion": 3,
"schemaVersion": 4,
"timeNextUpdateAvailable": null,
"timeUpdated": 12345678,
"type": "all"

View File

@ -4,6 +4,7 @@
"headerText": "builder",
"isFilterable": true,
"isSortable": true,
"useFreeformFilter": false,
"valuesAndCounts": [
[
"TODO",
@ -15,6 +16,7 @@
"headerText": "config",
"isFilterable": true,
"isSortable": true,
"useFreeformFilter": false,
"valuesAndCounts": [
[
"whole-image",
@ -26,6 +28,7 @@
"headerText": "resultType",
"isFilterable": true,
"isSortable": true,
"useFreeformFilter": false,
"valuesAndCounts": [
[
"failed",
@ -45,6 +48,7 @@
"headerText": "test",
"isFilterable": true,
"isSortable": true,
"useFreeformFilter": false,
"valuesAndCounts": [
[
"changed.skp",
@ -65,11 +69,17 @@
]
}
},
"extraColumnOrder": [
"builder",
"config",
"resultType",
"test"
],
"header": {
"dataHash": "-595743736412687673",
"isEditable": false,
"isExported": true,
"schemaVersion": 3,
"schemaVersion": 4,
"timeNextUpdateAvailable": null,
"timeUpdated": 12345678,
"type": "all"

View File

@ -4,6 +4,7 @@
"headerText": "builder",
"isFilterable": true,
"isSortable": true,
"useFreeformFilter": true,
"valuesAndCounts": [
[
"Test-Android-GalaxyNexus-SGX540-Arm7-Release",
@ -19,6 +20,7 @@
"headerText": "config",
"isFilterable": true,
"isSortable": true,
"useFreeformFilter": false,
"valuesAndCounts": [
[
"565",
@ -46,6 +48,7 @@
"headerText": "resultType",
"isFilterable": true,
"isSortable": true,
"useFreeformFilter": false,
"valuesAndCounts": [
[
"failed",
@ -69,6 +72,7 @@
"headerText": "test",
"isFilterable": true,
"isSortable": true,
"useFreeformFilter": true,
"valuesAndCounts": [
[
"3x3bitmaprect",
@ -109,11 +113,17 @@
]
}
},
"extraColumnOrder": [
"resultType",
"builder",
"test",
"config"
],
"header": {
"dataHash": "-7804718549064096650",
"isEditable": false,
"isExported": true,
"schemaVersion": 3,
"schemaVersion": 4,
"timeNextUpdateAvailable": null,
"timeUpdated": 12345678,
"type": "all"