blob: d2c7288a2fdb3601707ccb441cb35df33470e96f [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.org7498d952014-03-13 14:56:29 +000066 $scope.loadingMessage = "Loading results from '" + $scope.resultsToLoad +
epoger@google.comdcb4e652013-10-11 18:45:33 +000067 "', please wait...";
68
epoger@google.comad0e5522013-10-24 15:38:27 +000069 /**
70 * On initial page load, load a full dictionary of results.
71 * Once the dictionary is loaded, unhide the page elements so they can
72 * render the data.
73 */
commit-bot@chromium.org7498d952014-03-13 14:56:29 +000074 $http.get($scope.resultsToLoad).success(
epoger@google.comdcb4e652013-10-11 18:45:33 +000075 function(data, status, header, config) {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000076 var dataHeader = data[constants.KEY__HEADER];
77 if (dataHeader[constants.KEY__HEADER__IS_STILL_LOADING]) {
commit-bot@chromium.orged191072014-03-10 18:05:15 +000078 // Apply the server's requested reload delay to local time,
79 // so we will wait the right number of seconds regardless of clock
80 // skew between client and server.
81 var reloadDelayInSeconds =
82 dataHeader[constants.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE] -
83 dataHeader[constants.KEY__HEADER__TIME_UPDATED];
84 var timeNow = new Date().getTime();
85 var timeToReload = timeNow + reloadDelayInSeconds * 1000;
epoger@google.com2682c902013-12-05 16:05:16 +000086 $scope.loadingMessage =
commit-bot@chromium.org50ad8e42013-12-17 18:06:13 +000087 "Server is still loading results; will retry at " +
commit-bot@chromium.orged191072014-03-10 18:05:15 +000088 $scope.localTimeString(timeToReload / 1000);
epoger@google.com2682c902013-12-05 16:05:16 +000089 $timeout(
90 function(){location.reload();},
commit-bot@chromium.orged191072014-03-10 18:05:15 +000091 timeToReload - timeNow);
commit-bot@chromium.orgaddcdde2014-03-13 17:44:01 +000092 } else if (dataHeader[constants.KEY__HEADER__SCHEMA_VERSION] !=
93 constants.REBASELINE_SERVER_SCHEMA_VERSION_NUMBER) {
94 $scope.loadingMessage = "ERROR: Got JSON file with schema version "
95 + dataHeader[constants.KEY__HEADER__SCHEMA_VERSION]
96 + " but expected schema version "
97 + constants.REBASELINE_SERVER_SCHEMA_VERSION_NUMBER;
epoger@google.com2682c902013-12-05 16:05:16 +000098 } else {
99 $scope.loadingMessage = "Processing data, please wait...";
epoger@google.comdcb4e652013-10-11 18:45:33 +0000100
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000101 $scope.header = dataHeader;
102 $scope.extraColumnHeaders = data[constants.KEY__EXTRACOLUMNHEADERS];
103 $scope.imagePairs = data[constants.KEY__IMAGEPAIRS];
104 $scope.imageSets = data[constants.KEY__IMAGESETS];
105 $scope.sortColumnSubdict = constants.KEY__DIFFERENCE_DATA;
106 $scope.sortColumnKey = constants.KEY__DIFFERENCE_DATA__WEIGHTED_DIFF;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000107
epoger@google.com2682c902013-12-05 16:05:16 +0000108 $scope.showSubmitAdvancedSettings = false;
109 $scope.submitAdvancedSettings = {};
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000110 $scope.submitAdvancedSettings[
111 constants.KEY__EXPECTATIONS__REVIEWED] = true;
112 $scope.submitAdvancedSettings[
113 constants.KEY__EXPECTATIONS__IGNOREFAILURE] = false;
epoger@google.com2682c902013-12-05 16:05:16 +0000114 $scope.submitAdvancedSettings['bug'] = '';
epoger@google.com055e3b52013-10-26 14:31:11 +0000115
epoger@google.com2682c902013-12-05 16:05:16 +0000116 // Create the list of tabs (lists into which the user can file each
117 // test). This may vary, depending on isEditable.
118 $scope.tabs = [
119 'Unfiled', 'Hidden'
120 ];
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000121 if (dataHeader[constants.KEY__HEADER__IS_EDITABLE]) {
epoger@google.com2682c902013-12-05 16:05:16 +0000122 $scope.tabs = $scope.tabs.concat(
123 ['Pending Approval']);
124 }
125 $scope.defaultTab = $scope.tabs[0];
126 $scope.viewingTab = $scope.defaultTab;
127
128 // Track the number of results on each tab.
129 $scope.numResultsPerTab = {};
130 for (var i = 0; i < $scope.tabs.length; i++) {
131 $scope.numResultsPerTab[$scope.tabs[i]] = 0;
132 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000133 $scope.numResultsPerTab[$scope.defaultTab] = $scope.imagePairs.length;
epoger@google.com2682c902013-12-05 16:05:16 +0000134
135 // Add index and tab fields to all records.
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000136 for (var i = 0; i < $scope.imagePairs.length; i++) {
137 $scope.imagePairs[i].index = i;
138 $scope.imagePairs[i].tab = $scope.defaultTab;
epoger@google.com2682c902013-12-05 16:05:16 +0000139 }
140
141 // Arrays within which the user can toggle individual elements.
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000142 $scope.selectedImagePairs = [];
epoger@google.com2682c902013-12-05 16:05:16 +0000143
144 // Sets within which the user can toggle individual elements.
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000145 $scope.hiddenResultTypes = {};
146 $scope.hiddenResultTypes[
147 constants.KEY__RESULT_TYPE__FAILUREIGNORED] = true;
148 $scope.hiddenResultTypes[
149 constants.KEY__RESULT_TYPE__NOCOMPARISON] = true;
150 $scope.hiddenResultTypes[
151 constants.KEY__RESULT_TYPE__SUCCEEDED] = true;
152 $scope.allResultTypes = Object.keys(
153 $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMN__RESULT_TYPE]
154 [constants.KEY__VALUES_AND_COUNTS]);
epoger@google.com2682c902013-12-05 16:05:16 +0000155 $scope.hiddenConfigs = {};
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000156 $scope.allConfigs = Object.keys(
157 $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMN__CONFIG]
158 [constants.KEY__VALUES_AND_COUNTS]);
epoger@google.com2682c902013-12-05 16:05:16 +0000159
160 // Associative array of partial string matches per category.
161 $scope.categoryValueMatch = {};
162 $scope.categoryValueMatch.builder = "";
163 $scope.categoryValueMatch.test = "";
164
epoger@google.com62a5ef02013-12-05 18:03:24 +0000165 // If any defaults were overridden in the URL, get them now.
166 $scope.queryParameters.load();
167
epoger@google.com2682c902013-12-05 16:05:16 +0000168 $scope.updateResults();
169 $scope.loadingMessage = "";
170 $scope.windowTitle = "Current GM Results";
epoger@google.comeb832592013-10-23 15:07:26 +0000171 }
epoger@google.comdcb4e652013-10-11 18:45:33 +0000172 }
173 ).error(
174 function(data, status, header, config) {
commit-bot@chromium.org7498d952014-03-13 14:56:29 +0000175 $scope.loadingMessage = "Failed to load results from '"
epoger@google.com62a5ef02013-12-05 18:03:24 +0000176 + $scope.resultsToLoad + "'";
epoger@google.com542b65f2013-10-15 20:10:33 +0000177 $scope.windowTitle = "Failed to Load GM Results";
epoger@google.comf9d134d2013-09-27 15:02:44 +0000178 }
179 );
epoger@google.com5f2bb002013-10-02 18:57:48 +0000180
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000181
epoger@google.comad0e5522013-10-24 15:38:27 +0000182 //
epoger@google.com055e3b52013-10-26 14:31:11 +0000183 // Select/Clear/Toggle all tests.
184 //
185
186 /**
187 * Select all currently showing tests.
188 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000189 $scope.selectAllImagePairs = function() {
190 var numImagePairsShowing = $scope.limitedImagePairs.length;
191 for (var i = 0; i < numImagePairsShowing; i++) {
192 var index = $scope.limitedImagePairs[i].index;
193 if (!$scope.isValueInArray(index, $scope.selectedImagePairs)) {
194 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
epoger@google.com055e3b52013-10-26 14:31:11 +0000195 }
196 }
197 }
198
199 /**
200 * Deselect all currently showing tests.
201 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000202 $scope.clearAllImagePairs = function() {
203 var numImagePairsShowing = $scope.limitedImagePairs.length;
204 for (var i = 0; i < numImagePairsShowing; i++) {
205 var index = $scope.limitedImagePairs[i].index;
206 if ($scope.isValueInArray(index, $scope.selectedImagePairs)) {
207 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
epoger@google.com055e3b52013-10-26 14:31:11 +0000208 }
209 }
210 }
211
212 /**
213 * Toggle selection of all currently showing tests.
214 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000215 $scope.toggleAllImagePairs = function() {
216 var numImagePairsShowing = $scope.limitedImagePairs.length;
217 for (var i = 0; i < numImagePairsShowing; i++) {
218 var index = $scope.limitedImagePairs[i].index;
219 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
epoger@google.com055e3b52013-10-26 14:31:11 +0000220 }
221 }
222
223
224 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000225 // Tab operations.
226 //
epoger@google.com5f2bb002013-10-02 18:57:48 +0000227
epoger@google.comad0e5522013-10-24 15:38:27 +0000228 /**
229 * Change the selected tab.
230 *
231 * @param tab (string): name of the tab to select
232 */
epoger@google.comeb832592013-10-23 15:07:26 +0000233 $scope.setViewingTab = function(tab) {
234 $scope.viewingTab = tab;
235 $scope.updateResults();
236 }
237
epoger@google.comeb832592013-10-23 15:07:26 +0000238 /**
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000239 * Move the imagePairs in $scope.selectedImagePairs to a different tab,
240 * and then clear $scope.selectedImagePairs.
epoger@google.comeb832592013-10-23 15:07:26 +0000241 *
242 * @param newTab (string): name of the tab to move the tests to
243 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000244 $scope.moveSelectedImagePairsToTab = function(newTab) {
245 $scope.moveImagePairsToTab($scope.selectedImagePairs, newTab);
246 $scope.selectedImagePairs = [];
epoger@google.comeb832592013-10-23 15:07:26 +0000247 $scope.updateResults();
248 }
249
250 /**
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000251 * Move a subset of $scope.imagePairs to a different tab.
epoger@google.comeb832592013-10-23 15:07:26 +0000252 *
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000253 * @param imagePairIndices (array of ints): indices into $scope.imagePairs
epoger@google.comeb832592013-10-23 15:07:26 +0000254 * indicating which test results to move
255 * @param newTab (string): name of the tab to move the tests to
256 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000257 $scope.moveImagePairsToTab = function(imagePairIndices, newTab) {
258 var imagePairIndex;
259 var numImagePairs = imagePairIndices.length;
260 for (var i = 0; i < numImagePairs; i++) {
261 imagePairIndex = imagePairIndices[i];
262 $scope.numResultsPerTab[$scope.imagePairs[imagePairIndex].tab]--;
263 $scope.imagePairs[imagePairIndex].tab = newTab;
epoger@google.comeb832592013-10-23 15:07:26 +0000264 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000265 $scope.numResultsPerTab[newTab] += numImagePairs;
epoger@google.comeb832592013-10-23 15:07:26 +0000266 }
267
epoger@google.comad0e5522013-10-24 15:38:27 +0000268
269 //
epoger@google.com62a5ef02013-12-05 18:03:24 +0000270 // $scope.queryParameters:
271 // Transfer parameter values between $scope and the URL query string.
272 //
273 $scope.queryParameters = {};
274
275 // load and save functions for parameters of each type
276 // (load a parameter value into $scope from nameValuePairs,
277 // save a parameter value from $scope into nameValuePairs)
278 $scope.queryParameters.copiers = {
279 'simple': {
280 'load': function(nameValuePairs, name) {
281 var value = nameValuePairs[name];
282 if (value) {
283 $scope[name] = value;
284 }
285 },
286 'save': function(nameValuePairs, name) {
287 nameValuePairs[name] = $scope[name];
288 }
289 },
290
291 'categoryValueMatch': {
292 'load': function(nameValuePairs, name) {
293 var value = nameValuePairs[name];
294 if (value) {
295 $scope.categoryValueMatch[name] = value;
296 }
297 },
298 'save': function(nameValuePairs, name) {
299 nameValuePairs[name] = $scope.categoryValueMatch[name];
300 }
301 },
302
303 'set': {
304 'load': function(nameValuePairs, name) {
305 var value = nameValuePairs[name];
306 if (value) {
307 var valueArray = value.split(',');
308 $scope[name] = {};
309 $scope.toggleValuesInSet(valueArray, $scope[name]);
310 }
311 },
312 'save': function(nameValuePairs, name) {
313 nameValuePairs[name] = Object.keys($scope[name]).join(',');
314 }
315 },
316
317 };
318
319 // parameter name -> copier objects to load/save parameter value
320 $scope.queryParameters.map = {
commit-bot@chromium.org61f16062014-03-05 19:46:17 +0000321 'resultsToLoad': $scope.queryParameters.copiers.simple,
322 'displayLimitPending': $scope.queryParameters.copiers.simple,
323 'showThumbnailsPending': $scope.queryParameters.copiers.simple,
324 'imageSizePending': $scope.queryParameters.copiers.simple,
325 'sortColumnSubdict': $scope.queryParameters.copiers.simple,
326 'sortColumnKey': $scope.queryParameters.copiers.simple,
epoger@google.com62a5ef02013-12-05 18:03:24 +0000327
328 'hiddenResultTypes': $scope.queryParameters.copiers.set,
329 'hiddenConfigs': $scope.queryParameters.copiers.set,
330 };
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000331 $scope.queryParameters.map[constants.KEY__EXTRACOLUMN__BUILDER] =
332 $scope.queryParameters.copiers.categoryValueMatch;
333 $scope.queryParameters.map[constants.KEY__EXTRACOLUMN__TEST] =
334 $scope.queryParameters.copiers.categoryValueMatch;
epoger@google.com62a5ef02013-12-05 18:03:24 +0000335
336 // Loads all parameters into $scope from the URL query string;
337 // any which are not found within the URL will keep their current value.
338 $scope.queryParameters.load = function() {
339 var nameValuePairs = $location.search();
340 angular.forEach($scope.queryParameters.map,
341 function(copier, paramName) {
342 copier.load(nameValuePairs, paramName);
343 }
344 );
345 };
346
347 // Saves all parameters from $scope into the URL query string.
348 $scope.queryParameters.save = function() {
349 var nameValuePairs = {};
350 angular.forEach($scope.queryParameters.map,
351 function(copier, paramName) {
352 copier.save(nameValuePairs, paramName);
353 }
354 );
355 $location.search(nameValuePairs);
356 };
357
358
359 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000360 // updateResults() and friends.
361 //
362
363 /**
364 * Set $scope.areUpdatesPending (to enable/disable the Update Results
365 * button).
366 *
367 * TODO(epoger): We could reduce the amount of code by just setting the
368 * variable directly (from, e.g., a button's ng-click handler). But when
369 * I tried that, the HTML elements depending on the variable did not get
370 * updated.
371 * It turns out that this is due to variable scoping within an ng-repeat
372 * element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat
373 *
374 * @param val boolean value to set $scope.areUpdatesPending to
375 */
376 $scope.setUpdatesPending = function(val) {
377 $scope.areUpdatesPending = val;
378 }
379
380 /**
epoger@google.com62a5ef02013-12-05 18:03:24 +0000381 * Update the displayed results, based on filters/settings,
382 * and call $scope.queryParameters.save() so that the new filter results
383 * can be bookmarked.
epoger@google.comad0e5522013-10-24 15:38:27 +0000384 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000385 $scope.updateResults = function() {
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +0000386 $scope.renderStartTime = window.performance.now();
387 $log.debug("renderStartTime: " + $scope.renderStartTime);
epoger@google.com5f2bb002013-10-02 18:57:48 +0000388 $scope.displayLimit = $scope.displayLimitPending;
389 // TODO(epoger): Every time we apply a filter, AngularJS creates
390 // another copy of the array. Is there a way we can filter out
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000391 // the imagePairs as they are displayed, rather than storing multiple
epoger@google.com5f2bb002013-10-02 18:57:48 +0000392 // array copies? (For better performance.)
epoger@google.comeb832592013-10-23 15:07:26 +0000393
394 if ($scope.viewingTab == $scope.defaultTab) {
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000395
396 // TODO(epoger): Until we allow the user to reverse sort order,
397 // there are certain columns we want to sort in a different order.
398 var doReverse = (
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000399 ($scope.sortColumnKey ==
400 constants.KEY__DIFFERENCE_DATA__PERCENT_DIFF_PIXELS) ||
401 ($scope.sortColumnKey ==
402 constants.KEY__DIFFERENCE_DATA__WEIGHTED_DIFF));
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000403
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000404 $scope.filteredImagePairs =
epoger@google.comeb832592013-10-23 15:07:26 +0000405 $filter("orderBy")(
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000406 $filter("removeHiddenImagePairs")(
407 $scope.imagePairs,
epoger@google.comeb832592013-10-23 15:07:26 +0000408 $scope.hiddenResultTypes,
409 $scope.hiddenConfigs,
epoger@google.comf4394d52013-10-29 15:49:40 +0000410 $scope.categoryValueMatch.builder,
411 $scope.categoryValueMatch.test,
epoger@google.comeb832592013-10-23 15:07:26 +0000412 $scope.viewingTab
413 ),
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000414 $scope.getSortColumnValue, doReverse);
415 $scope.limitedImagePairs = $filter("limitTo")(
416 $scope.filteredImagePairs, $scope.displayLimit);
epoger@google.comeb832592013-10-23 15:07:26 +0000417 } else {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000418 $scope.filteredImagePairs =
epoger@google.comeb832592013-10-23 15:07:26 +0000419 $filter("orderBy")(
420 $filter("filter")(
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000421 $scope.imagePairs,
epoger@google.comeb832592013-10-23 15:07:26 +0000422 {tab: $scope.viewingTab},
423 true
424 ),
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000425 $scope.getSortColumnValue);
426 $scope.limitedImagePairs = $scope.filteredImagePairs;
epoger@google.comeb832592013-10-23 15:07:26 +0000427 }
commit-bot@chromium.org61f16062014-03-05 19:46:17 +0000428 $scope.showThumbnails = $scope.showThumbnailsPending;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000429 $scope.imageSize = $scope.imageSizePending;
epoger@google.comad0e5522013-10-24 15:38:27 +0000430 $scope.setUpdatesPending(false);
epoger@google.com62a5ef02013-12-05 18:03:24 +0000431 $scope.queryParameters.save();
epoger@google.com5f2bb002013-10-02 18:57:48 +0000432 }
433
epoger@google.comad0e5522013-10-24 15:38:27 +0000434 /**
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +0000435 * This function is called when the results have been completely rendered
436 * after updateResults().
437 */
438 $scope.resultsUpdatedCallback = function() {
439 $scope.renderEndTime = window.performance.now();
440 $log.debug("renderEndTime: " + $scope.renderEndTime);
441 }
442
443 /**
epoger@google.comad0e5522013-10-24 15:38:27 +0000444 * Re-sort the displayed results.
445 *
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000446 * @param subdict (string): which subdictionary
447 * (constants.KEY__DIFFERENCE_DATA, constants.KEY__EXPECTATIONS_DATA,
448 * constants.KEY__EXTRA_COLUMN_VALUES) the sort column key is within
449 * @param key (string): sort by value associated with this key in subdict
epoger@google.comad0e5522013-10-24 15:38:27 +0000450 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000451 $scope.sortResultsBy = function(subdict, key) {
452 $scope.sortColumnSubdict = subdict;
453 $scope.sortColumnKey = key;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000454 $scope.updateResults();
455 }
epoger@google.comeb832592013-10-23 15:07:26 +0000456
epoger@google.comf4394d52013-10-29 15:49:40 +0000457 /**
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000458 * For a particular ImagePair, return the value of the column we are
459 * sorting on (according to $scope.sortColumnSubdict and
460 * $scope.sortColumnKey).
461 *
462 * @param imagePair: imagePair to get a column value out of.
463 */
464 $scope.getSortColumnValue = function(imagePair) {
465 if ($scope.sortColumnSubdict in imagePair) {
466 return imagePair[$scope.sortColumnSubdict][$scope.sortColumnKey];
467 } else {
468 return undefined;
469 }
470 }
471
472 /**
epoger@google.comf4394d52013-10-29 15:49:40 +0000473 * Set $scope.categoryValueMatch[name] = value, and update results.
474 *
475 * @param name
476 * @param value
477 */
478 $scope.setCategoryValueMatch = function(name, value) {
479 $scope.categoryValueMatch[name] = value;
480 $scope.updateResults();
481 }
482
483 /**
484 * Update $scope.hiddenResultTypes so that ONLY this resultType is showing,
485 * and update the visible results.
486 *
487 * @param resultType
488 */
489 $scope.showOnlyResultType = function(resultType) {
490 $scope.hiddenResultTypes = {};
491 // TODO(epoger): Maybe change $scope.allResultTypes to be a Set like
492 // $scope.hiddenResultTypes (rather than an array), so this operation is
493 // simpler (just assign or add allResultTypes to hiddenResultTypes).
494 $scope.toggleValuesInSet($scope.allResultTypes, $scope.hiddenResultTypes);
495 $scope.toggleValueInSet(resultType, $scope.hiddenResultTypes);
496 $scope.updateResults();
497 }
498
499 /**
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000500 * Update $scope.hiddenResultTypes so that ALL resultTypes are showing,
501 * and update the visible results.
502 */
503 $scope.showAllResultTypes = function() {
504 $scope.hiddenResultTypes = {};
505 $scope.updateResults();
506 }
507
508 /**
epoger@google.comf4394d52013-10-29 15:49:40 +0000509 * Update $scope.hiddenConfigs so that ONLY this config is showing,
510 * and update the visible results.
511 *
512 * @param config
513 */
514 $scope.showOnlyConfig = function(config) {
515 $scope.hiddenConfigs = {};
516 $scope.toggleValuesInSet($scope.allConfigs, $scope.hiddenConfigs);
517 $scope.toggleValueInSet(config, $scope.hiddenConfigs);
518 $scope.updateResults();
519 }
520
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000521 /**
522 * Update $scope.hiddenConfigs so that ALL configs are showing,
523 * and update the visible results.
524 */
525 $scope.showAllConfigs = function() {
526 $scope.hiddenConfigs = {};
527 $scope.updateResults();
528 }
529
epoger@google.comad0e5522013-10-24 15:38:27 +0000530
531 //
532 // Operations for sending info back to the server.
533 //
534
epoger@google.comeb832592013-10-23 15:07:26 +0000535 /**
536 * Tell the server that the actual results of these particular tests
537 * are acceptable.
538 *
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000539 * TODO(epoger): This assumes that the original expectations are in
540 * imageSetA, and the actuals are in imageSetB.
541 *
542 * @param imagePairsSubset an array of test results, most likely a subset of
543 * $scope.imagePairs (perhaps with some modifications)
epoger@google.comeb832592013-10-23 15:07:26 +0000544 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000545 $scope.submitApprovals = function(imagePairsSubset) {
epoger@google.comeb832592013-10-23 15:07:26 +0000546 $scope.submitPending = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000547
548 // Convert bug text field to null or 1-item array.
549 var bugs = null;
550 var bugNumber = parseInt($scope.submitAdvancedSettings['bug']);
551 if (!isNaN(bugNumber)) {
552 bugs = [bugNumber];
553 }
554
555 // TODO(epoger): This is a suboptimal way to prevent users from
556 // rebaselining failures in alternative renderModes, but it does work.
557 // For a better solution, see
558 // https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new
559 // result type, RenderModeMismatch')
560 var encounteredComparisonConfig = false;
561
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000562 var updatedExpectations = [];
563 for (var i = 0; i < imagePairsSubset.length; i++) {
564 var imagePair = imagePairsSubset[i];
565 var updatedExpectation = {};
566 updatedExpectation[constants.KEY__EXPECTATIONS_DATA] =
567 imagePair[constants.KEY__EXPECTATIONS_DATA];
568 updatedExpectation[constants.KEY__EXTRA_COLUMN_VALUES] =
569 imagePair[constants.KEY__EXTRA_COLUMN_VALUES];
570 updatedExpectation[constants.KEY__NEW_IMAGE_URL] =
571 imagePair[constants.KEY__IMAGE_B_URL];
572 if (0 == updatedExpectation[constants.KEY__EXTRA_COLUMN_VALUES]
573 [constants.KEY__EXTRACOLUMN__CONFIG]
574 .indexOf('comparison-')) {
epoger@google.com055e3b52013-10-26 14:31:11 +0000575 encounteredComparisonConfig = true;
576 }
577
578 // Advanced settings...
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000579 if (null == updatedExpectation[constants.KEY__EXPECTATIONS_DATA]) {
580 updatedExpectation[constants.KEY__EXPECTATIONS_DATA] = {};
epoger@google.com055e3b52013-10-26 14:31:11 +0000581 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000582 updatedExpectation[constants.KEY__EXPECTATIONS_DATA]
583 [constants.KEY__EXPECTATIONS__REVIEWED] =
584 $scope.submitAdvancedSettings[
585 constants.KEY__EXPECTATIONS__REVIEWED];
586 if (true == $scope.submitAdvancedSettings[
587 constants.KEY__EXPECTATIONS__IGNOREFAILURE]) {
588 // if it's false, don't send it at all (just keep the default)
589 updatedExpectation[constants.KEY__EXPECTATIONS_DATA]
590 [constants.KEY__EXPECTATIONS__IGNOREFAILURE] = true;
591 }
592 updatedExpectation[constants.KEY__EXPECTATIONS_DATA]
593 [constants.KEY__EXPECTATIONS__BUGS] = bugs;
epoger@google.com055e3b52013-10-26 14:31:11 +0000594
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000595 updatedExpectations.push(updatedExpectation);
epoger@google.comeb832592013-10-23 15:07:26 +0000596 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000597 if (encounteredComparisonConfig) {
598 alert("Approval failed -- you cannot approve results with config " +
599 "type comparison-*");
600 $scope.submitPending = false;
601 return;
602 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000603 var modificationData = {};
604 modificationData[constants.KEY__EDITS__MODIFICATIONS] =
605 updatedExpectations;
606 modificationData[constants.KEY__EDITS__OLD_RESULTS_HASH] =
607 $scope.header[constants.KEY__HEADER__DATAHASH];
608 modificationData[constants.KEY__EDITS__OLD_RESULTS_TYPE] =
609 $scope.header[constants.KEY__HEADER__TYPE];
epoger@google.comeb832592013-10-23 15:07:26 +0000610 $http({
611 method: "POST",
612 url: "/edits",
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000613 data: modificationData
epoger@google.comeb832592013-10-23 15:07:26 +0000614 }).success(function(data, status, headers, config) {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000615 var imagePairIndicesToMove = [];
616 for (var i = 0; i < imagePairsSubset.length; i++) {
617 imagePairIndicesToMove.push(imagePairsSubset[i].index);
epoger@google.comeb832592013-10-23 15:07:26 +0000618 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000619 $scope.moveImagePairsToTab(imagePairIndicesToMove,
620 "HackToMakeSureThisImagePairDisappears");
epoger@google.comeb832592013-10-23 15:07:26 +0000621 $scope.updateResults();
622 alert("New baselines submitted successfully!\n\n" +
623 "You still need to commit the updated expectations files on " +
624 "the server side to the Skia repo.\n\n" +
commit-bot@chromium.org50ad8e42013-12-17 18:06:13 +0000625 "When you click OK, your web UI will reload; after that " +
626 "completes, you will see the updated data (once the server has " +
627 "finished loading the update results into memory!) and you can " +
628 "submit more baselines if you want.");
629 // I don't know why, but if I just call reload() here it doesn't work.
630 // Making a timer call it fixes the problem.
631 $timeout(function(){location.reload();}, 1);
epoger@google.comeb832592013-10-23 15:07:26 +0000632 }).error(function(data, status, headers, config) {
633 alert("There was an error submitting your baselines.\n\n" +
634 "Please see server-side log for details.");
635 $scope.submitPending = false;
636 });
637 }
epoger@google.comad0e5522013-10-24 15:38:27 +0000638
639
640 //
641 // Operations we use to mimic Set semantics, in such a way that
642 // checking for presence within the Set is as fast as possible.
643 // But getting a list of all values within the Set is not necessarily
644 // possible.
645 // TODO(epoger): move into a separate .js file?
646 //
647
648 /**
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000649 * Returns the number of values present within set "set".
650 *
651 * @param set an Object which we use to mimic set semantics
652 */
653 $scope.setSize = function(set) {
654 return Object.keys(set).length;
655 }
656
657 /**
epoger@google.comad0e5522013-10-24 15:38:27 +0000658 * Returns true if value "value" is present within set "set".
659 *
660 * @param value a value of any type
661 * @param set an Object which we use to mimic set semantics
662 * (this should make isValueInSet faster than if we used an Array)
663 */
664 $scope.isValueInSet = function(value, set) {
665 return (true == set[value]);
666 }
667
668 /**
669 * If value "value" is already in set "set", remove it; otherwise, add it.
670 *
671 * @param value a value of any type
672 * @param set an Object which we use to mimic set semantics
673 */
674 $scope.toggleValueInSet = function(value, set) {
675 if (true == set[value]) {
676 delete set[value];
677 } else {
678 set[value] = true;
679 }
680 }
681
epoger@google.comf4394d52013-10-29 15:49:40 +0000682 /**
683 * For each value in valueArray, call toggleValueInSet(value, set).
684 *
685 * @param valueArray
686 * @param set
687 */
688 $scope.toggleValuesInSet = function(valueArray, set) {
689 var arrayLength = valueArray.length;
690 for (var i = 0; i < arrayLength; i++) {
691 $scope.toggleValueInSet(valueArray[i], set);
692 }
693 }
694
epoger@google.comad0e5522013-10-24 15:38:27 +0000695
696 //
697 // Array operations; similar to our Set operations, but operate on a
698 // Javascript Array so we *can* easily get a list of all values in the Set.
699 // TODO(epoger): move into a separate .js file?
700 //
701
702 /**
703 * Returns true if value "value" is present within array "array".
704 *
705 * @param value a value of any type
706 * @param array a Javascript Array
707 */
708 $scope.isValueInArray = function(value, array) {
709 return (-1 != array.indexOf(value));
710 }
711
712 /**
713 * If value "value" is already in array "array", remove it; otherwise,
714 * add it.
715 *
716 * @param value a value of any type
717 * @param array a Javascript Array
718 */
719 $scope.toggleValueInArray = function(value, array) {
720 var i = array.indexOf(value);
721 if (-1 == i) {
722 array.push(value);
723 } else {
724 array.splice(i, 1);
725 }
726 }
727
728
729 //
730 // Miscellaneous utility functions.
731 // TODO(epoger): move into a separate .js file?
732 //
733
734 /**
735 * Returns a human-readable (in local time zone) time string for a
736 * particular moment in time.
737 *
738 * @param secondsPastEpoch (numeric): seconds past epoch in UTC
739 */
740 $scope.localTimeString = function(secondsPastEpoch) {
741 var d = new Date(secondsPastEpoch * 1000);
742 return d.toString();
743 }
744
commit-bot@chromium.orgceba0792014-02-05 19:49:17 +0000745 /**
746 * Returns a hex color string (such as "#aabbcc") for the given RGB values.
747 *
748 * @param r (numeric): red channel value, 0-255
749 * @param g (numeric): green channel value, 0-255
750 * @param b (numeric): blue channel value, 0-255
751 */
752 $scope.hexColorString = function(r, g, b) {
753 var rString = r.toString(16);
754 if (r < 16) {
755 rString = "0" + rString;
756 }
757 var gString = g.toString(16);
758 if (g < 16) {
759 gString = "0" + gString;
760 }
761 var bString = b.toString(16);
762 if (b < 16) {
763 bString = "0" + bString;
764 }
765 return '#' + rString + gString + bString;
766 }
767
768 /**
769 * Returns a hex color string (such as "#aabbcc") for the given brightness.
770 *
771 * @param brightnessString (string): 0-255, 0 is completely black
772 *
773 * TODO(epoger): It might be nice to tint the color when it's not completely
774 * black or completely white.
775 */
776 $scope.brightnessStringToHexColor = function(brightnessString) {
777 var v = parseInt(brightnessString);
778 return $scope.hexColorString(v, v, v);
779 }
780
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000781 /**
782 * Returns the last path component of image diff URL for a given ImagePair.
783 *
784 * Depending on which diff this is (whitediffs, pixeldiffs, etc.) this
785 * will be relative to different base URLs.
786 *
787 * We must keep this function in sync with _get_difference_locator() in
788 * ../imagediffdb.py
789 *
790 * @param imagePair: ImagePair to generate image diff URL for
791 */
792 $scope.getImageDiffRelativeUrl = function(imagePair) {
793 var before =
794 imagePair[constants.KEY__IMAGE_A_URL] + "-vs-" +
795 imagePair[constants.KEY__IMAGE_B_URL];
796 return before.replace(/[^\w\-]/g, "_") + ".png";
797 }
798
epoger@google.comf9d134d2013-09-27 15:02:44 +0000799 }
800);