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'
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.
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.
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
# 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
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'
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)
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,
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),
}
# 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'
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: {
'headerText': 'builder',
'isFilterable': True,
'isSortable': True,
+ 'useFreeformFilter': False,
'valuesAndCounts': [('MyBuilder', 3)],
},
'test': {
'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,
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):
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."""
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
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):
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)
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)
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
// 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',
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',
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);
}
}
$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;
// 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();
}
},
- '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];
}
},
};
- // 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() {
$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
$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],
}
/**
- * 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]);
</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"
<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>
<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]}}
- <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]}}">
- 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]}}
+
+ <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__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]}}">
+ 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__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}}"
"headerText": "builder",
"isFilterable": true,
"isSortable": true,
+ "useFreeformFilter": false,
"valuesAndCounts": [
[
"Test-Android-GalaxyNexus-SGX540-Arm7-Release",
"headerText": "config",
"isFilterable": true,
"isSortable": true,
+ "useFreeformFilter": false,
"valuesAndCounts": [
[
"TODO",
"headerText": "resultType",
"isFilterable": true,
"isSortable": true,
+ "useFreeformFilter": false,
"valuesAndCounts": [
[
"failed",
"headerText": "test",
"isFilterable": true,
"isSortable": true,
+ "useFreeformFilter": false,
"valuesAndCounts": [
[
"3x3bitmaprect",
]
}
},
+ "extraColumnOrder": [
+ "builder",
+ "config",
+ "resultType",
+ "test"
+ ],
"header": {
"dataHash": "-5829724510169924592",
"isEditable": false,
"isExported": true,
- "schemaVersion": 3,
+ "schemaVersion": 4,
"timeNextUpdateAvailable": null,
"timeUpdated": 12345678,
"type": "all"
"headerText": "builder",
"isFilterable": true,
"isSortable": true,
+ "useFreeformFilter": false,
"valuesAndCounts": [
[
"TODO",
"headerText": "config",
"isFilterable": true,
"isSortable": true,
+ "useFreeformFilter": false,
"valuesAndCounts": [
[
"whole-image",
"headerText": "resultType",
"isFilterable": true,
"isSortable": true,
+ "useFreeformFilter": false,
"valuesAndCounts": [
[
"failed",
"headerText": "test",
"isFilterable": true,
"isSortable": true,
+ "useFreeformFilter": false,
"valuesAndCounts": [
[
"changed.skp",
]
}
},
+ "extraColumnOrder": [
+ "builder",
+ "config",
+ "resultType",
+ "test"
+ ],
"header": {
"dataHash": "-595743736412687673",
"isEditable": false,
"isExported": true,
- "schemaVersion": 3,
+ "schemaVersion": 4,
"timeNextUpdateAvailable": null,
"timeUpdated": 12345678,
"type": "all"
"headerText": "builder",
"isFilterable": true,
"isSortable": true,
+ "useFreeformFilter": true,
"valuesAndCounts": [
[
"Test-Android-GalaxyNexus-SGX540-Arm7-Release",
"headerText": "config",
"isFilterable": true,
"isSortable": true,
+ "useFreeformFilter": false,
"valuesAndCounts": [
[
"565",
"headerText": "resultType",
"isFilterable": true,
"isSortable": true,
+ "useFreeformFilter": false,
"valuesAndCounts": [
[
"failed",
"headerText": "test",
"isFilterable": true,
"isSortable": true,
+ "useFreeformFilter": true,
"valuesAndCounts": [
[
"3x3bitmaprect",
]
}
},
+ "extraColumnOrder": [
+ "resultType",
+ "builder",
+ "test",
+ "config"
+ ],
"header": {
"dataHash": "-7804718549064096650",
"isEditable": false,
"isExported": true,
- "schemaVersion": 3,
+ "schemaVersion": 4,
"timeNextUpdateAvailable": null,
"timeUpdated": 12345678,
"type": "all"