blob: fea5aa0404632ad52797464a99a27c6ce1e6b988 [file] [log] [blame]
epoger@google.comf9d134d2013-09-27 15:02:44 +00001/*
2 * Loader:
epoger@google.comafaad3d2013-09-30 15:06:25 +00003 * Reads GM result reports written out by results.py, and imports
commit-bot@chromium.org16f41802014-02-26 19:05:20 +00004 * them into $scope.extraColumnHeaders and $scope.imagePairs .
epoger@google.comf9d134d2013-09-27 15:02:44 +00005 */
6var Loader = angular.module(
7 'Loader',
commit-bot@chromium.org6e3ee332014-03-10 19:12:53 +00008 ['ConstantsModule']
epoger@google.comf9d134d2013-09-27 15:02:44 +00009);
epoger@google.com5f2bb002013-10-02 18:57:48 +000010
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +000011Loader.directive(
12 'resultsUpdatedCallbackDirective',
13 ['$timeout',
14 function($timeout) {
15 return function(scope, element, attrs) {
16 if (scope.$last) {
17 $timeout(function() {
18 scope.resultsUpdatedCallback();
19 });
20 }
21 };
22 }
23 ]
24);
25
epoger@google.com5f2bb002013-10-02 18:57:48 +000026// TODO(epoger): Combine ALL of our filtering operations (including
27// truncation) into this one filter, so that runs most efficiently?
28// (We would have to make sure truncation still took place after
29// sorting, though.)
30Loader.filter(
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000031 'removeHiddenImagePairs',
32 function(constants) {
33 return function(unfilteredImagePairs, hiddenResultTypes, hiddenConfigs,
epoger@google.comf4394d52013-10-29 15:49:40 +000034 builderSubstring, testSubstring, viewingTab) {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000035 var filteredImagePairs = [];
36 for (var i = 0; i < unfilteredImagePairs.length; i++) {
37 var imagePair = unfilteredImagePairs[i];
38 var extraColumnValues = imagePair[constants.KEY__EXTRA_COLUMN_VALUES];
epoger@google.com055e3b52013-10-26 14:31:11 +000039 // For performance, we examine the "set" objects directly rather
40 // than calling $scope.isValueInSet().
41 // Besides, I don't think we have access to $scope in here...
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000042 if (!(true == hiddenResultTypes[extraColumnValues[
43 constants.KEY__EXTRACOLUMN__RESULT_TYPE]]) &&
44 !(true == hiddenConfigs[extraColumnValues[
45 constants.KEY__EXTRACOLUMN__CONFIG]]) &&
46 !(-1 == extraColumnValues[constants.KEY__EXTRACOLUMN__BUILDER]
47 .indexOf(builderSubstring)) &&
48 !(-1 == extraColumnValues[constants.KEY__EXTRACOLUMN__TEST]
49 .indexOf(testSubstring)) &&
50 (viewingTab == imagePair.tab)) {
51 filteredImagePairs.push(imagePair);
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000052 }
epoger@google.com5f2bb002013-10-02 18:57:48 +000053 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000054 return filteredImagePairs;
epoger@google.com5f2bb002013-10-02 18:57:48 +000055 };
56 }
57);
58
epoger@google.comad0e5522013-10-24 15:38:27 +000059
epoger@google.comf9d134d2013-09-27 15:02:44 +000060Loader.controller(
61 'Loader.Controller',
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +000062 function($scope, $http, $filter, $location, $log, $timeout, constants) {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000063 $scope.constants = constants;
epoger@google.com542b65f2013-10-15 20:10:33 +000064 $scope.windowTitle = "Loading GM Results...";
epoger@google.com62a5ef02013-12-05 18:03:24 +000065 $scope.resultsToLoad = $location.search().resultsToLoad;
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +000066 $scope.loadingMessage = "please wait...";
epoger@google.comdcb4e652013-10-11 18:45:33 +000067
epoger@google.comad0e5522013-10-24 15:38:27 +000068 /**
69 * On initial page load, load a full dictionary of results.
70 * Once the dictionary is loaded, unhide the page elements so they can
71 * render the data.
72 */
commit-bot@chromium.org7498d952014-03-13 14:56:29 +000073 $http.get($scope.resultsToLoad).success(
epoger@google.comdcb4e652013-10-11 18:45:33 +000074 function(data, status, header, config) {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000075 var dataHeader = data[constants.KEY__HEADER];
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +000076 if (dataHeader[constants.KEY__HEADER__SCHEMA_VERSION] !=
77 constants.REBASELINE_SERVER_SCHEMA_VERSION_NUMBER) {
78 $scope.loadingMessage = "ERROR: Got JSON file with schema version "
79 + dataHeader[constants.KEY__HEADER__SCHEMA_VERSION]
80 + " but expected schema version "
81 + constants.REBASELINE_SERVER_SCHEMA_VERSION_NUMBER;
82 } else if (dataHeader[constants.KEY__HEADER__IS_STILL_LOADING]) {
commit-bot@chromium.orged191072014-03-10 18:05:15 +000083 // Apply the server's requested reload delay to local time,
84 // so we will wait the right number of seconds regardless of clock
85 // skew between client and server.
86 var reloadDelayInSeconds =
87 dataHeader[constants.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE] -
88 dataHeader[constants.KEY__HEADER__TIME_UPDATED];
89 var timeNow = new Date().getTime();
90 var timeToReload = timeNow + reloadDelayInSeconds * 1000;
epoger@google.com2682c902013-12-05 16:05:16 +000091 $scope.loadingMessage =
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +000092 "server is still loading results; will retry at " +
commit-bot@chromium.orged191072014-03-10 18:05:15 +000093 $scope.localTimeString(timeToReload / 1000);
epoger@google.com2682c902013-12-05 16:05:16 +000094 $timeout(
95 function(){location.reload();},
commit-bot@chromium.orged191072014-03-10 18:05:15 +000096 timeToReload - timeNow);
epoger@google.com2682c902013-12-05 16:05:16 +000097 } else {
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +000098 $scope.loadingMessage = "processing data, please wait...";
epoger@google.comdcb4e652013-10-11 18:45:33 +000099
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000100 $scope.header = dataHeader;
101 $scope.extraColumnHeaders = data[constants.KEY__EXTRACOLUMNHEADERS];
102 $scope.imagePairs = data[constants.KEY__IMAGEPAIRS];
103 $scope.imageSets = data[constants.KEY__IMAGESETS];
104 $scope.sortColumnSubdict = constants.KEY__DIFFERENCE_DATA;
105 $scope.sortColumnKey = constants.KEY__DIFFERENCE_DATA__WEIGHTED_DIFF;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000106
epoger@google.com2682c902013-12-05 16:05:16 +0000107 $scope.showSubmitAdvancedSettings = false;
108 $scope.submitAdvancedSettings = {};
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000109 $scope.submitAdvancedSettings[
110 constants.KEY__EXPECTATIONS__REVIEWED] = true;
111 $scope.submitAdvancedSettings[
112 constants.KEY__EXPECTATIONS__IGNOREFAILURE] = false;
epoger@google.com2682c902013-12-05 16:05:16 +0000113 $scope.submitAdvancedSettings['bug'] = '';
epoger@google.com055e3b52013-10-26 14:31:11 +0000114
epoger@google.com2682c902013-12-05 16:05:16 +0000115 // Create the list of tabs (lists into which the user can file each
116 // test). This may vary, depending on isEditable.
117 $scope.tabs = [
118 'Unfiled', 'Hidden'
119 ];
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000120 if (dataHeader[constants.KEY__HEADER__IS_EDITABLE]) {
epoger@google.com2682c902013-12-05 16:05:16 +0000121 $scope.tabs = $scope.tabs.concat(
122 ['Pending Approval']);
123 }
124 $scope.defaultTab = $scope.tabs[0];
125 $scope.viewingTab = $scope.defaultTab;
126
127 // Track the number of results on each tab.
128 $scope.numResultsPerTab = {};
129 for (var i = 0; i < $scope.tabs.length; i++) {
130 $scope.numResultsPerTab[$scope.tabs[i]] = 0;
131 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000132 $scope.numResultsPerTab[$scope.defaultTab] = $scope.imagePairs.length;
epoger@google.com2682c902013-12-05 16:05:16 +0000133
134 // Add index and tab fields to all records.
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000135 for (var i = 0; i < $scope.imagePairs.length; i++) {
136 $scope.imagePairs[i].index = i;
137 $scope.imagePairs[i].tab = $scope.defaultTab;
epoger@google.com2682c902013-12-05 16:05:16 +0000138 }
139
140 // Arrays within which the user can toggle individual elements.
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000141 $scope.selectedImagePairs = [];
epoger@google.com2682c902013-12-05 16:05:16 +0000142
143 // Sets within which the user can toggle individual elements.
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000144 $scope.hiddenResultTypes = {};
145 $scope.hiddenResultTypes[
146 constants.KEY__RESULT_TYPE__FAILUREIGNORED] = true;
147 $scope.hiddenResultTypes[
148 constants.KEY__RESULT_TYPE__NOCOMPARISON] = true;
149 $scope.hiddenResultTypes[
150 constants.KEY__RESULT_TYPE__SUCCEEDED] = true;
151 $scope.allResultTypes = Object.keys(
152 $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMN__RESULT_TYPE]
153 [constants.KEY__VALUES_AND_COUNTS]);
epoger@google.com2682c902013-12-05 16:05:16 +0000154 $scope.hiddenConfigs = {};
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000155 $scope.allConfigs = Object.keys(
156 $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMN__CONFIG]
157 [constants.KEY__VALUES_AND_COUNTS]);
epoger@google.com2682c902013-12-05 16:05:16 +0000158
159 // Associative array of partial string matches per category.
160 $scope.categoryValueMatch = {};
161 $scope.categoryValueMatch.builder = "";
162 $scope.categoryValueMatch.test = "";
163
epoger@google.com62a5ef02013-12-05 18:03:24 +0000164 // If any defaults were overridden in the URL, get them now.
165 $scope.queryParameters.load();
166
epoger@google.com2682c902013-12-05 16:05:16 +0000167 $scope.updateResults();
168 $scope.loadingMessage = "";
169 $scope.windowTitle = "Current GM Results";
epoger@google.comeb832592013-10-23 15:07:26 +0000170 }
epoger@google.comdcb4e652013-10-11 18:45:33 +0000171 }
172 ).error(
173 function(data, status, header, config) {
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +0000174 $scope.loadingMessage = "FAILED to load.";
epoger@google.com542b65f2013-10-15 20:10:33 +0000175 $scope.windowTitle = "Failed to Load GM Results";
epoger@google.comf9d134d2013-09-27 15:02:44 +0000176 }
177 );
epoger@google.com5f2bb002013-10-02 18:57:48 +0000178
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000179
epoger@google.comad0e5522013-10-24 15:38:27 +0000180 //
epoger@google.com055e3b52013-10-26 14:31:11 +0000181 // Select/Clear/Toggle all tests.
182 //
183
184 /**
185 * Select all currently showing tests.
186 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000187 $scope.selectAllImagePairs = function() {
188 var numImagePairsShowing = $scope.limitedImagePairs.length;
189 for (var i = 0; i < numImagePairsShowing; i++) {
190 var index = $scope.limitedImagePairs[i].index;
191 if (!$scope.isValueInArray(index, $scope.selectedImagePairs)) {
192 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
epoger@google.com055e3b52013-10-26 14:31:11 +0000193 }
194 }
195 }
196
197 /**
198 * Deselect all currently showing tests.
199 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000200 $scope.clearAllImagePairs = function() {
201 var numImagePairsShowing = $scope.limitedImagePairs.length;
202 for (var i = 0; i < numImagePairsShowing; i++) {
203 var index = $scope.limitedImagePairs[i].index;
204 if ($scope.isValueInArray(index, $scope.selectedImagePairs)) {
205 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
epoger@google.com055e3b52013-10-26 14:31:11 +0000206 }
207 }
208 }
209
210 /**
211 * Toggle selection of all currently showing tests.
212 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000213 $scope.toggleAllImagePairs = function() {
214 var numImagePairsShowing = $scope.limitedImagePairs.length;
215 for (var i = 0; i < numImagePairsShowing; i++) {
216 var index = $scope.limitedImagePairs[i].index;
217 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
epoger@google.com055e3b52013-10-26 14:31:11 +0000218 }
219 }
220
221
222 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000223 // Tab operations.
224 //
epoger@google.com5f2bb002013-10-02 18:57:48 +0000225
epoger@google.comad0e5522013-10-24 15:38:27 +0000226 /**
227 * Change the selected tab.
228 *
229 * @param tab (string): name of the tab to select
230 */
epoger@google.comeb832592013-10-23 15:07:26 +0000231 $scope.setViewingTab = function(tab) {
232 $scope.viewingTab = tab;
233 $scope.updateResults();
234 }
235
epoger@google.comeb832592013-10-23 15:07:26 +0000236 /**
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000237 * Move the imagePairs in $scope.selectedImagePairs to a different tab,
238 * and then clear $scope.selectedImagePairs.
epoger@google.comeb832592013-10-23 15:07:26 +0000239 *
240 * @param newTab (string): name of the tab to move the tests to
241 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000242 $scope.moveSelectedImagePairsToTab = function(newTab) {
243 $scope.moveImagePairsToTab($scope.selectedImagePairs, newTab);
244 $scope.selectedImagePairs = [];
epoger@google.comeb832592013-10-23 15:07:26 +0000245 $scope.updateResults();
246 }
247
248 /**
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000249 * Move a subset of $scope.imagePairs to a different tab.
epoger@google.comeb832592013-10-23 15:07:26 +0000250 *
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000251 * @param imagePairIndices (array of ints): indices into $scope.imagePairs
epoger@google.comeb832592013-10-23 15:07:26 +0000252 * indicating which test results to move
253 * @param newTab (string): name of the tab to move the tests to
254 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000255 $scope.moveImagePairsToTab = function(imagePairIndices, newTab) {
256 var imagePairIndex;
257 var numImagePairs = imagePairIndices.length;
258 for (var i = 0; i < numImagePairs; i++) {
259 imagePairIndex = imagePairIndices[i];
260 $scope.numResultsPerTab[$scope.imagePairs[imagePairIndex].tab]--;
261 $scope.imagePairs[imagePairIndex].tab = newTab;
epoger@google.comeb832592013-10-23 15:07:26 +0000262 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000263 $scope.numResultsPerTab[newTab] += numImagePairs;
epoger@google.comeb832592013-10-23 15:07:26 +0000264 }
265
epoger@google.comad0e5522013-10-24 15:38:27 +0000266
267 //
epoger@google.com62a5ef02013-12-05 18:03:24 +0000268 // $scope.queryParameters:
269 // Transfer parameter values between $scope and the URL query string.
270 //
271 $scope.queryParameters = {};
272
273 // load and save functions for parameters of each type
274 // (load a parameter value into $scope from nameValuePairs,
275 // save a parameter value from $scope into nameValuePairs)
276 $scope.queryParameters.copiers = {
277 'simple': {
278 'load': function(nameValuePairs, name) {
279 var value = nameValuePairs[name];
280 if (value) {
281 $scope[name] = value;
282 }
283 },
284 'save': function(nameValuePairs, name) {
285 nameValuePairs[name] = $scope[name];
286 }
287 },
288
289 'categoryValueMatch': {
290 'load': function(nameValuePairs, name) {
291 var value = nameValuePairs[name];
292 if (value) {
293 $scope.categoryValueMatch[name] = value;
294 }
295 },
296 'save': function(nameValuePairs, name) {
297 nameValuePairs[name] = $scope.categoryValueMatch[name];
298 }
299 },
300
301 'set': {
302 'load': function(nameValuePairs, name) {
303 var value = nameValuePairs[name];
304 if (value) {
305 var valueArray = value.split(',');
306 $scope[name] = {};
307 $scope.toggleValuesInSet(valueArray, $scope[name]);
308 }
309 },
310 'save': function(nameValuePairs, name) {
311 nameValuePairs[name] = Object.keys($scope[name]).join(',');
312 }
313 },
314
315 };
316
317 // parameter name -> copier objects to load/save parameter value
318 $scope.queryParameters.map = {
commit-bot@chromium.org61f16062014-03-05 19:46:17 +0000319 'resultsToLoad': $scope.queryParameters.copiers.simple,
320 'displayLimitPending': $scope.queryParameters.copiers.simple,
321 'showThumbnailsPending': $scope.queryParameters.copiers.simple,
322 'imageSizePending': $scope.queryParameters.copiers.simple,
323 'sortColumnSubdict': $scope.queryParameters.copiers.simple,
324 'sortColumnKey': $scope.queryParameters.copiers.simple,
epoger@google.com62a5ef02013-12-05 18:03:24 +0000325
326 'hiddenResultTypes': $scope.queryParameters.copiers.set,
327 'hiddenConfigs': $scope.queryParameters.copiers.set,
328 };
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000329 $scope.queryParameters.map[constants.KEY__EXTRACOLUMN__BUILDER] =
330 $scope.queryParameters.copiers.categoryValueMatch;
331 $scope.queryParameters.map[constants.KEY__EXTRACOLUMN__TEST] =
332 $scope.queryParameters.copiers.categoryValueMatch;
epoger@google.com62a5ef02013-12-05 18:03:24 +0000333
334 // Loads all parameters into $scope from the URL query string;
335 // any which are not found within the URL will keep their current value.
336 $scope.queryParameters.load = function() {
337 var nameValuePairs = $location.search();
338 angular.forEach($scope.queryParameters.map,
339 function(copier, paramName) {
340 copier.load(nameValuePairs, paramName);
341 }
342 );
343 };
344
345 // Saves all parameters from $scope into the URL query string.
346 $scope.queryParameters.save = function() {
347 var nameValuePairs = {};
348 angular.forEach($scope.queryParameters.map,
349 function(copier, paramName) {
350 copier.save(nameValuePairs, paramName);
351 }
352 );
353 $location.search(nameValuePairs);
354 };
355
356
357 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000358 // updateResults() and friends.
359 //
360
361 /**
362 * Set $scope.areUpdatesPending (to enable/disable the Update Results
363 * button).
364 *
365 * TODO(epoger): We could reduce the amount of code by just setting the
366 * variable directly (from, e.g., a button's ng-click handler). But when
367 * I tried that, the HTML elements depending on the variable did not get
368 * updated.
369 * It turns out that this is due to variable scoping within an ng-repeat
370 * element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat
371 *
372 * @param val boolean value to set $scope.areUpdatesPending to
373 */
374 $scope.setUpdatesPending = function(val) {
375 $scope.areUpdatesPending = val;
376 }
377
378 /**
epoger@google.com62a5ef02013-12-05 18:03:24 +0000379 * Update the displayed results, based on filters/settings,
380 * and call $scope.queryParameters.save() so that the new filter results
381 * can be bookmarked.
epoger@google.comad0e5522013-10-24 15:38:27 +0000382 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000383 $scope.updateResults = function() {
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +0000384 $scope.renderStartTime = window.performance.now();
385 $log.debug("renderStartTime: " + $scope.renderStartTime);
epoger@google.com5f2bb002013-10-02 18:57:48 +0000386 $scope.displayLimit = $scope.displayLimitPending;
387 // TODO(epoger): Every time we apply a filter, AngularJS creates
388 // another copy of the array. Is there a way we can filter out
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000389 // the imagePairs as they are displayed, rather than storing multiple
epoger@google.com5f2bb002013-10-02 18:57:48 +0000390 // array copies? (For better performance.)
epoger@google.comeb832592013-10-23 15:07:26 +0000391
392 if ($scope.viewingTab == $scope.defaultTab) {
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000393
394 // TODO(epoger): Until we allow the user to reverse sort order,
395 // there are certain columns we want to sort in a different order.
396 var doReverse = (
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000397 ($scope.sortColumnKey ==
398 constants.KEY__DIFFERENCE_DATA__PERCENT_DIFF_PIXELS) ||
399 ($scope.sortColumnKey ==
400 constants.KEY__DIFFERENCE_DATA__WEIGHTED_DIFF));
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000401
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000402 $scope.filteredImagePairs =
epoger@google.comeb832592013-10-23 15:07:26 +0000403 $filter("orderBy")(
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000404 $filter("removeHiddenImagePairs")(
405 $scope.imagePairs,
epoger@google.comeb832592013-10-23 15:07:26 +0000406 $scope.hiddenResultTypes,
407 $scope.hiddenConfigs,
epoger@google.comf4394d52013-10-29 15:49:40 +0000408 $scope.categoryValueMatch.builder,
409 $scope.categoryValueMatch.test,
epoger@google.comeb832592013-10-23 15:07:26 +0000410 $scope.viewingTab
411 ),
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000412 $scope.getSortColumnValue, doReverse);
413 $scope.limitedImagePairs = $filter("limitTo")(
414 $scope.filteredImagePairs, $scope.displayLimit);
epoger@google.comeb832592013-10-23 15:07:26 +0000415 } else {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000416 $scope.filteredImagePairs =
epoger@google.comeb832592013-10-23 15:07:26 +0000417 $filter("orderBy")(
418 $filter("filter")(
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000419 $scope.imagePairs,
epoger@google.comeb832592013-10-23 15:07:26 +0000420 {tab: $scope.viewingTab},
421 true
422 ),
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000423 $scope.getSortColumnValue);
424 $scope.limitedImagePairs = $scope.filteredImagePairs;
epoger@google.comeb832592013-10-23 15:07:26 +0000425 }
commit-bot@chromium.org61f16062014-03-05 19:46:17 +0000426 $scope.showThumbnails = $scope.showThumbnailsPending;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000427 $scope.imageSize = $scope.imageSizePending;
epoger@google.comad0e5522013-10-24 15:38:27 +0000428 $scope.setUpdatesPending(false);
epoger@google.com62a5ef02013-12-05 18:03:24 +0000429 $scope.queryParameters.save();
epoger@google.com5f2bb002013-10-02 18:57:48 +0000430 }
431
epoger@google.comad0e5522013-10-24 15:38:27 +0000432 /**
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +0000433 * This function is called when the results have been completely rendered
434 * after updateResults().
435 */
436 $scope.resultsUpdatedCallback = function() {
437 $scope.renderEndTime = window.performance.now();
438 $log.debug("renderEndTime: " + $scope.renderEndTime);
439 }
440
441 /**
epoger@google.comad0e5522013-10-24 15:38:27 +0000442 * Re-sort the displayed results.
443 *
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000444 * @param subdict (string): which subdictionary
445 * (constants.KEY__DIFFERENCE_DATA, constants.KEY__EXPECTATIONS_DATA,
446 * constants.KEY__EXTRA_COLUMN_VALUES) the sort column key is within
447 * @param key (string): sort by value associated with this key in subdict
epoger@google.comad0e5522013-10-24 15:38:27 +0000448 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000449 $scope.sortResultsBy = function(subdict, key) {
450 $scope.sortColumnSubdict = subdict;
451 $scope.sortColumnKey = key;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000452 $scope.updateResults();
453 }
epoger@google.comeb832592013-10-23 15:07:26 +0000454
epoger@google.comf4394d52013-10-29 15:49:40 +0000455 /**
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000456 * For a particular ImagePair, return the value of the column we are
457 * sorting on (according to $scope.sortColumnSubdict and
458 * $scope.sortColumnKey).
459 *
460 * @param imagePair: imagePair to get a column value out of.
461 */
462 $scope.getSortColumnValue = function(imagePair) {
463 if ($scope.sortColumnSubdict in imagePair) {
464 return imagePair[$scope.sortColumnSubdict][$scope.sortColumnKey];
465 } else {
466 return undefined;
467 }
468 }
469
470 /**
epoger@google.comf4394d52013-10-29 15:49:40 +0000471 * Set $scope.categoryValueMatch[name] = value, and update results.
472 *
473 * @param name
474 * @param value
475 */
476 $scope.setCategoryValueMatch = function(name, value) {
477 $scope.categoryValueMatch[name] = value;
478 $scope.updateResults();
479 }
480
481 /**
482 * Update $scope.hiddenResultTypes so that ONLY this resultType is showing,
483 * and update the visible results.
484 *
485 * @param resultType
486 */
487 $scope.showOnlyResultType = function(resultType) {
488 $scope.hiddenResultTypes = {};
489 // TODO(epoger): Maybe change $scope.allResultTypes to be a Set like
490 // $scope.hiddenResultTypes (rather than an array), so this operation is
491 // simpler (just assign or add allResultTypes to hiddenResultTypes).
492 $scope.toggleValuesInSet($scope.allResultTypes, $scope.hiddenResultTypes);
493 $scope.toggleValueInSet(resultType, $scope.hiddenResultTypes);
494 $scope.updateResults();
495 }
496
497 /**
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000498 * Update $scope.hiddenResultTypes so that ALL resultTypes are showing,
499 * and update the visible results.
500 */
501 $scope.showAllResultTypes = function() {
502 $scope.hiddenResultTypes = {};
503 $scope.updateResults();
504 }
505
506 /**
epoger@google.comf4394d52013-10-29 15:49:40 +0000507 * Update $scope.hiddenConfigs so that ONLY this config is showing,
508 * and update the visible results.
509 *
510 * @param config
511 */
512 $scope.showOnlyConfig = function(config) {
513 $scope.hiddenConfigs = {};
514 $scope.toggleValuesInSet($scope.allConfigs, $scope.hiddenConfigs);
515 $scope.toggleValueInSet(config, $scope.hiddenConfigs);
516 $scope.updateResults();
517 }
518
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000519 /**
520 * Update $scope.hiddenConfigs so that ALL configs are showing,
521 * and update the visible results.
522 */
523 $scope.showAllConfigs = function() {
524 $scope.hiddenConfigs = {};
525 $scope.updateResults();
526 }
527
epoger@google.comad0e5522013-10-24 15:38:27 +0000528
529 //
530 // Operations for sending info back to the server.
531 //
532
epoger@google.comeb832592013-10-23 15:07:26 +0000533 /**
534 * Tell the server that the actual results of these particular tests
535 * are acceptable.
536 *
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000537 * TODO(epoger): This assumes that the original expectations are in
538 * imageSetA, and the actuals are in imageSetB.
539 *
540 * @param imagePairsSubset an array of test results, most likely a subset of
541 * $scope.imagePairs (perhaps with some modifications)
epoger@google.comeb832592013-10-23 15:07:26 +0000542 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000543 $scope.submitApprovals = function(imagePairsSubset) {
epoger@google.comeb832592013-10-23 15:07:26 +0000544 $scope.submitPending = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000545
546 // Convert bug text field to null or 1-item array.
547 var bugs = null;
548 var bugNumber = parseInt($scope.submitAdvancedSettings['bug']);
549 if (!isNaN(bugNumber)) {
550 bugs = [bugNumber];
551 }
552
553 // TODO(epoger): This is a suboptimal way to prevent users from
554 // rebaselining failures in alternative renderModes, but it does work.
555 // For a better solution, see
556 // https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new
557 // result type, RenderModeMismatch')
558 var encounteredComparisonConfig = false;
559
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000560 var updatedExpectations = [];
561 for (var i = 0; i < imagePairsSubset.length; i++) {
562 var imagePair = imagePairsSubset[i];
563 var updatedExpectation = {};
564 updatedExpectation[constants.KEY__EXPECTATIONS_DATA] =
565 imagePair[constants.KEY__EXPECTATIONS_DATA];
566 updatedExpectation[constants.KEY__EXTRA_COLUMN_VALUES] =
567 imagePair[constants.KEY__EXTRA_COLUMN_VALUES];
568 updatedExpectation[constants.KEY__NEW_IMAGE_URL] =
569 imagePair[constants.KEY__IMAGE_B_URL];
570 if (0 == updatedExpectation[constants.KEY__EXTRA_COLUMN_VALUES]
571 [constants.KEY__EXTRACOLUMN__CONFIG]
572 .indexOf('comparison-')) {
epoger@google.com055e3b52013-10-26 14:31:11 +0000573 encounteredComparisonConfig = true;
574 }
575
576 // Advanced settings...
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000577 if (null == updatedExpectation[constants.KEY__EXPECTATIONS_DATA]) {
578 updatedExpectation[constants.KEY__EXPECTATIONS_DATA] = {};
epoger@google.com055e3b52013-10-26 14:31:11 +0000579 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000580 updatedExpectation[constants.KEY__EXPECTATIONS_DATA]
581 [constants.KEY__EXPECTATIONS__REVIEWED] =
582 $scope.submitAdvancedSettings[
583 constants.KEY__EXPECTATIONS__REVIEWED];
584 if (true == $scope.submitAdvancedSettings[
585 constants.KEY__EXPECTATIONS__IGNOREFAILURE]) {
586 // if it's false, don't send it at all (just keep the default)
587 updatedExpectation[constants.KEY__EXPECTATIONS_DATA]
588 [constants.KEY__EXPECTATIONS__IGNOREFAILURE] = true;
589 }
590 updatedExpectation[constants.KEY__EXPECTATIONS_DATA]
591 [constants.KEY__EXPECTATIONS__BUGS] = bugs;
epoger@google.com055e3b52013-10-26 14:31:11 +0000592
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000593 updatedExpectations.push(updatedExpectation);
epoger@google.comeb832592013-10-23 15:07:26 +0000594 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000595 if (encounteredComparisonConfig) {
596 alert("Approval failed -- you cannot approve results with config " +
597 "type comparison-*");
598 $scope.submitPending = false;
599 return;
600 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000601 var modificationData = {};
602 modificationData[constants.KEY__EDITS__MODIFICATIONS] =
603 updatedExpectations;
604 modificationData[constants.KEY__EDITS__OLD_RESULTS_HASH] =
605 $scope.header[constants.KEY__HEADER__DATAHASH];
606 modificationData[constants.KEY__EDITS__OLD_RESULTS_TYPE] =
607 $scope.header[constants.KEY__HEADER__TYPE];
epoger@google.comeb832592013-10-23 15:07:26 +0000608 $http({
609 method: "POST",
610 url: "/edits",
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000611 data: modificationData
epoger@google.comeb832592013-10-23 15:07:26 +0000612 }).success(function(data, status, headers, config) {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000613 var imagePairIndicesToMove = [];
614 for (var i = 0; i < imagePairsSubset.length; i++) {
615 imagePairIndicesToMove.push(imagePairsSubset[i].index);
epoger@google.comeb832592013-10-23 15:07:26 +0000616 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000617 $scope.moveImagePairsToTab(imagePairIndicesToMove,
618 "HackToMakeSureThisImagePairDisappears");
epoger@google.comeb832592013-10-23 15:07:26 +0000619 $scope.updateResults();
620 alert("New baselines submitted successfully!\n\n" +
621 "You still need to commit the updated expectations files on " +
622 "the server side to the Skia repo.\n\n" +
commit-bot@chromium.org50ad8e42013-12-17 18:06:13 +0000623 "When you click OK, your web UI will reload; after that " +
624 "completes, you will see the updated data (once the server has " +
625 "finished loading the update results into memory!) and you can " +
626 "submit more baselines if you want.");
627 // I don't know why, but if I just call reload() here it doesn't work.
628 // Making a timer call it fixes the problem.
629 $timeout(function(){location.reload();}, 1);
epoger@google.comeb832592013-10-23 15:07:26 +0000630 }).error(function(data, status, headers, config) {
631 alert("There was an error submitting your baselines.\n\n" +
632 "Please see server-side log for details.");
633 $scope.submitPending = false;
634 });
635 }
epoger@google.comad0e5522013-10-24 15:38:27 +0000636
637
638 //
639 // Operations we use to mimic Set semantics, in such a way that
640 // checking for presence within the Set is as fast as possible.
641 // But getting a list of all values within the Set is not necessarily
642 // possible.
643 // TODO(epoger): move into a separate .js file?
644 //
645
646 /**
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000647 * Returns the number of values present within set "set".
648 *
649 * @param set an Object which we use to mimic set semantics
650 */
651 $scope.setSize = function(set) {
652 return Object.keys(set).length;
653 }
654
655 /**
epoger@google.comad0e5522013-10-24 15:38:27 +0000656 * Returns true if value "value" is present within set "set".
657 *
658 * @param value a value of any type
659 * @param set an Object which we use to mimic set semantics
660 * (this should make isValueInSet faster than if we used an Array)
661 */
662 $scope.isValueInSet = function(value, set) {
663 return (true == set[value]);
664 }
665
666 /**
667 * If value "value" is already in set "set", remove it; otherwise, add it.
668 *
669 * @param value a value of any type
670 * @param set an Object which we use to mimic set semantics
671 */
672 $scope.toggleValueInSet = function(value, set) {
673 if (true == set[value]) {
674 delete set[value];
675 } else {
676 set[value] = true;
677 }
678 }
679
epoger@google.comf4394d52013-10-29 15:49:40 +0000680 /**
681 * For each value in valueArray, call toggleValueInSet(value, set).
682 *
683 * @param valueArray
684 * @param set
685 */
686 $scope.toggleValuesInSet = function(valueArray, set) {
687 var arrayLength = valueArray.length;
688 for (var i = 0; i < arrayLength; i++) {
689 $scope.toggleValueInSet(valueArray[i], set);
690 }
691 }
692
epoger@google.comad0e5522013-10-24 15:38:27 +0000693
694 //
695 // Array operations; similar to our Set operations, but operate on a
696 // Javascript Array so we *can* easily get a list of all values in the Set.
697 // TODO(epoger): move into a separate .js file?
698 //
699
700 /**
701 * Returns true if value "value" is present within array "array".
702 *
703 * @param value a value of any type
704 * @param array a Javascript Array
705 */
706 $scope.isValueInArray = function(value, array) {
707 return (-1 != array.indexOf(value));
708 }
709
710 /**
711 * If value "value" is already in array "array", remove it; otherwise,
712 * add it.
713 *
714 * @param value a value of any type
715 * @param array a Javascript Array
716 */
717 $scope.toggleValueInArray = function(value, array) {
718 var i = array.indexOf(value);
719 if (-1 == i) {
720 array.push(value);
721 } else {
722 array.splice(i, 1);
723 }
724 }
725
726
727 //
728 // Miscellaneous utility functions.
729 // TODO(epoger): move into a separate .js file?
730 //
731
732 /**
733 * Returns a human-readable (in local time zone) time string for a
734 * particular moment in time.
735 *
736 * @param secondsPastEpoch (numeric): seconds past epoch in UTC
737 */
738 $scope.localTimeString = function(secondsPastEpoch) {
739 var d = new Date(secondsPastEpoch * 1000);
740 return d.toString();
741 }
742
commit-bot@chromium.orgceba0792014-02-05 19:49:17 +0000743 /**
744 * Returns a hex color string (such as "#aabbcc") for the given RGB values.
745 *
746 * @param r (numeric): red channel value, 0-255
747 * @param g (numeric): green channel value, 0-255
748 * @param b (numeric): blue channel value, 0-255
749 */
750 $scope.hexColorString = function(r, g, b) {
751 var rString = r.toString(16);
752 if (r < 16) {
753 rString = "0" + rString;
754 }
755 var gString = g.toString(16);
756 if (g < 16) {
757 gString = "0" + gString;
758 }
759 var bString = b.toString(16);
760 if (b < 16) {
761 bString = "0" + bString;
762 }
763 return '#' + rString + gString + bString;
764 }
765
766 /**
767 * Returns a hex color string (such as "#aabbcc") for the given brightness.
768 *
769 * @param brightnessString (string): 0-255, 0 is completely black
770 *
771 * TODO(epoger): It might be nice to tint the color when it's not completely
772 * black or completely white.
773 */
774 $scope.brightnessStringToHexColor = function(brightnessString) {
775 var v = parseInt(brightnessString);
776 return $scope.hexColorString(v, v, v);
777 }
778
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000779 /**
780 * Returns the last path component of image diff URL for a given ImagePair.
781 *
782 * Depending on which diff this is (whitediffs, pixeldiffs, etc.) this
783 * will be relative to different base URLs.
784 *
785 * We must keep this function in sync with _get_difference_locator() in
786 * ../imagediffdb.py
787 *
788 * @param imagePair: ImagePair to generate image diff URL for
789 */
790 $scope.getImageDiffRelativeUrl = function(imagePair) {
791 var before =
792 imagePair[constants.KEY__IMAGE_A_URL] + "-vs-" +
793 imagePair[constants.KEY__IMAGE_B_URL];
794 return before.replace(/[^\w\-]/g, "_") + ".png";
795 }
796
epoger@google.comf9d134d2013-09-27 15:02:44 +0000797 }
798);