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:
parent
874a62acef
commit
b4edbffd7c
@ -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
|
||||
|
@ -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),
|
||||
}
|
||||
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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]);
|
||||
|
@ -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}}"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user