blob: 5af0e24c2bc1b65636d169dff99880b08d008cad [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
4 * them into $scope.categories and $scope.testData .
epoger@google.comf9d134d2013-09-27 15:02:44 +00005 */
6var Loader = angular.module(
7 'Loader',
8 []
9);
epoger@google.com5f2bb002013-10-02 18:57:48 +000010
epoger@google.comad0e5522013-10-24 15:38:27 +000011
epoger@google.com5f2bb002013-10-02 18:57:48 +000012// TODO(epoger): Combine ALL of our filtering operations (including
13// truncation) into this one filter, so that runs most efficiently?
14// (We would have to make sure truncation still took place after
15// sorting, though.)
16Loader.filter(
17 'removeHiddenItems',
18 function() {
epoger@google.comeb832592013-10-23 15:07:26 +000019 return function(unfilteredItems, hiddenResultTypes, hiddenConfigs,
20 viewingTab) {
epoger@google.com5f2bb002013-10-02 18:57:48 +000021 var filteredItems = [];
22 for (var i = 0; i < unfilteredItems.length; i++) {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000023 var item = unfilteredItems[i];
epoger@google.comad0e5522013-10-24 15:38:27 +000024 // For performance, we examine the "set" objects directly rather
25 // than calling $scope.isValueInSet().
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000026 if (!(true == hiddenResultTypes[item.resultType]) &&
epoger@google.comeb832592013-10-23 15:07:26 +000027 !(true == hiddenConfigs[item.config]) &&
28 (viewingTab == item.tab)) {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000029 filteredItems.push(item);
30 }
epoger@google.com5f2bb002013-10-02 18:57:48 +000031 }
32 return filteredItems;
33 };
34 }
35);
36
epoger@google.comad0e5522013-10-24 15:38:27 +000037
epoger@google.comf9d134d2013-09-27 15:02:44 +000038Loader.controller(
39 'Loader.Controller',
epoger@google.com542b65f2013-10-15 20:10:33 +000040 function($scope, $http, $filter, $location) {
41 $scope.windowTitle = "Loading GM Results...";
epoger@google.comdcb4e652013-10-11 18:45:33 +000042 var resultsToLoad = $location.search().resultsToLoad;
43 $scope.loadingMessage = "Loading results of type '" + resultsToLoad +
44 "', please wait...";
45
epoger@google.comad0e5522013-10-24 15:38:27 +000046 /**
47 * On initial page load, load a full dictionary of results.
48 * Once the dictionary is loaded, unhide the page elements so they can
49 * render the data.
50 */
epoger@google.comdcb4e652013-10-11 18:45:33 +000051 $http.get("/results/" + resultsToLoad).success(
52 function(data, status, header, config) {
53 $scope.loadingMessage = "Processing data, please wait...";
54
55 $scope.header = data.header;
56 $scope.categories = data.categories;
57 $scope.testData = data.testData;
epoger@google.comf9d134d2013-09-27 15:02:44 +000058 $scope.sortColumn = 'test';
epoger@google.comeb832592013-10-23 15:07:26 +000059 $scope.showTodos = false;
epoger@google.com5f2bb002013-10-02 18:57:48 +000060
epoger@google.comeb832592013-10-23 15:07:26 +000061 // Create the list of tabs (lists into which the user can file each
62 // test). This may vary, depending on isEditable.
63 $scope.tabs = [
64 'Unfiled', 'Hidden'
65 ];
66 if (data.header.isEditable) {
67 $scope.tabs = $scope.tabs.concat(
68 ['Pending Approval']);
69 }
70 $scope.defaultTab = $scope.tabs[0];
71 $scope.viewingTab = $scope.defaultTab;
72
73 // Track the number of results on each tab.
74 $scope.numResultsPerTab = {};
75 for (var i = 0; i < $scope.tabs.length; i++) {
76 $scope.numResultsPerTab[$scope.tabs[i]] = 0;
77 }
78 $scope.numResultsPerTab[$scope.defaultTab] = $scope.testData.length;
79
80 // Add index and tab fields to all records.
epoger@google.comdcb4e652013-10-11 18:45:33 +000081 for (var i = 0; i < $scope.testData.length; i++) {
82 $scope.testData[i].index = i;
epoger@google.comeb832592013-10-23 15:07:26 +000083 $scope.testData[i].tab = $scope.defaultTab;
epoger@google.comdcb4e652013-10-11 18:45:33 +000084 }
85
epoger@google.comad0e5522013-10-24 15:38:27 +000086 // Arrays within which the user can toggle individual elements.
87 $scope.selectedItems = [];
88
89 // Sets within which the user can toggle individual elements.
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000090 $scope.hiddenResultTypes = {
91 'failure-ignored': true,
92 'no-comparison': true,
93 'succeeded': true,
94 };
95 $scope.hiddenConfigs = {};
epoger@google.com5f2bb002013-10-02 18:57:48 +000096
97 $scope.updateResults();
epoger@google.comdcb4e652013-10-11 18:45:33 +000098 $scope.loadingMessage = "";
epoger@google.com542b65f2013-10-15 20:10:33 +000099 $scope.windowTitle = "Current GM Results";
epoger@google.comdcb4e652013-10-11 18:45:33 +0000100 }
101 ).error(
102 function(data, status, header, config) {
103 $scope.loadingMessage = "Failed to load results of type '"
104 + resultsToLoad + "'";
epoger@google.com542b65f2013-10-15 20:10:33 +0000105 $scope.windowTitle = "Failed to Load GM Results";
epoger@google.comf9d134d2013-09-27 15:02:44 +0000106 }
107 );
epoger@google.com5f2bb002013-10-02 18:57:48 +0000108
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000109
epoger@google.comad0e5522013-10-24 15:38:27 +0000110 //
111 // Tab operations.
112 //
epoger@google.com5f2bb002013-10-02 18:57:48 +0000113
epoger@google.comad0e5522013-10-24 15:38:27 +0000114 /**
115 * Change the selected tab.
116 *
117 * @param tab (string): name of the tab to select
118 */
epoger@google.comeb832592013-10-23 15:07:26 +0000119 $scope.setViewingTab = function(tab) {
120 $scope.viewingTab = tab;
121 $scope.updateResults();
122 }
123
epoger@google.comeb832592013-10-23 15:07:26 +0000124 /**
125 * Move the items in $scope.selectedItems to a different tab,
126 * and then clear $scope.selectedItems.
127 *
128 * @param newTab (string): name of the tab to move the tests to
129 */
130 $scope.moveSelectedItemsToTab = function(newTab) {
131 $scope.moveItemsToTab($scope.selectedItems, newTab);
132 $scope.selectedItems = [];
133 $scope.updateResults();
134 }
135
136 /**
137 * Move a subset of $scope.testData to a different tab.
138 *
139 * @param itemIndices (array of ints): indices into $scope.testData
140 * indicating which test results to move
141 * @param newTab (string): name of the tab to move the tests to
142 */
143 $scope.moveItemsToTab = function(itemIndices, newTab) {
144 var itemIndex;
145 var numItems = itemIndices.length;
146 for (var i = 0; i < numItems; i++) {
147 itemIndex = itemIndices[i];
148 $scope.numResultsPerTab[$scope.testData[itemIndex].tab]--;
149 $scope.testData[itemIndex].tab = newTab;
150 }
151 $scope.numResultsPerTab[newTab] += numItems;
152 }
153
epoger@google.comad0e5522013-10-24 15:38:27 +0000154
155 //
156 // updateResults() and friends.
157 //
158
159 /**
160 * Set $scope.areUpdatesPending (to enable/disable the Update Results
161 * button).
162 *
163 * TODO(epoger): We could reduce the amount of code by just setting the
164 * variable directly (from, e.g., a button's ng-click handler). But when
165 * I tried that, the HTML elements depending on the variable did not get
166 * updated.
167 * It turns out that this is due to variable scoping within an ng-repeat
168 * element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat
169 *
170 * @param val boolean value to set $scope.areUpdatesPending to
171 */
172 $scope.setUpdatesPending = function(val) {
173 $scope.areUpdatesPending = val;
174 }
175
176 /**
177 * Update the displayed results, based on filters/settings.
178 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000179 $scope.updateResults = function() {
180 $scope.displayLimit = $scope.displayLimitPending;
181 // TODO(epoger): Every time we apply a filter, AngularJS creates
182 // another copy of the array. Is there a way we can filter out
183 // the items as they are displayed, rather than storing multiple
184 // array copies? (For better performance.)
epoger@google.comeb832592013-10-23 15:07:26 +0000185
186 if ($scope.viewingTab == $scope.defaultTab) {
187 $scope.filteredTestData =
188 $filter("orderBy")(
189 $filter("removeHiddenItems")(
190 $scope.testData,
191 $scope.hiddenResultTypes,
192 $scope.hiddenConfigs,
193 $scope.viewingTab
194 ),
195 $scope.sortColumn);
196 $scope.limitedTestData = $filter("limitTo")(
197 $scope.filteredTestData, $scope.displayLimit);
198 } else {
199 $scope.filteredTestData =
200 $filter("orderBy")(
201 $filter("filter")(
202 $scope.testData,
203 {tab: $scope.viewingTab},
204 true
205 ),
206 $scope.sortColumn);
207 $scope.limitedTestData = $filter("limitTo")(
208 $scope.filteredTestData, $scope.displayLimit);
209 }
epoger@google.com5f2bb002013-10-02 18:57:48 +0000210 $scope.imageSize = $scope.imageSizePending;
epoger@google.comad0e5522013-10-24 15:38:27 +0000211 $scope.setUpdatesPending(false);
epoger@google.com5f2bb002013-10-02 18:57:48 +0000212 }
213
epoger@google.comad0e5522013-10-24 15:38:27 +0000214 /**
215 * Re-sort the displayed results.
216 *
217 * @param sortColumn (string): name of the column to sort on
218 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000219 $scope.sortResultsBy = function(sortColumn) {
220 $scope.sortColumn = sortColumn;
221 $scope.updateResults();
222 }
epoger@google.comeb832592013-10-23 15:07:26 +0000223
epoger@google.comad0e5522013-10-24 15:38:27 +0000224
225 //
226 // Operations for sending info back to the server.
227 //
228
epoger@google.comeb832592013-10-23 15:07:26 +0000229 /**
230 * Tell the server that the actual results of these particular tests
231 * are acceptable.
232 *
233 * @param testDataSubset an array of test results, most likely a subset of
234 * $scope.testData (perhaps with some modifications)
235 */
236 $scope.submitApprovals = function(testDataSubset) {
237 $scope.submitPending = true;
238 var newResults = [];
239 for (var i = 0; i < testDataSubset.length; i++) {
240 var actualResult = testDataSubset[i];
241 var expectedResult = {
242 builder: actualResult['builder'],
243 test: actualResult['test'],
244 config: actualResult['config'],
245 expectedHashType: actualResult['actualHashType'],
246 expectedHashDigest: actualResult['actualHashDigest'],
247 };
248 newResults.push(expectedResult);
249 }
250 $http({
251 method: "POST",
252 url: "/edits",
253 data: {
254 oldResultsType: $scope.header.type,
255 oldResultsHash: $scope.header.dataHash,
256 modifications: newResults
257 }
258 }).success(function(data, status, headers, config) {
259 var itemIndicesToMove = [];
260 for (var i = 0; i < testDataSubset.length; i++) {
261 itemIndicesToMove.push(testDataSubset[i].index);
262 }
263 $scope.moveItemsToTab(itemIndicesToMove,
264 "HackToMakeSureThisItemDisappears");
265 $scope.updateResults();
266 alert("New baselines submitted successfully!\n\n" +
267 "You still need to commit the updated expectations files on " +
268 "the server side to the Skia repo.\n\n" +
269 "Also: in order to see the complete updated data, or to submit " +
270 "more baselines, you will need to reload your client.");
271 $scope.submitPending = false;
272 }).error(function(data, status, headers, config) {
273 alert("There was an error submitting your baselines.\n\n" +
274 "Please see server-side log for details.");
275 $scope.submitPending = false;
276 });
277 }
epoger@google.comad0e5522013-10-24 15:38:27 +0000278
279
280 //
281 // Operations we use to mimic Set semantics, in such a way that
282 // checking for presence within the Set is as fast as possible.
283 // But getting a list of all values within the Set is not necessarily
284 // possible.
285 // TODO(epoger): move into a separate .js file?
286 //
287
288 /**
289 * Returns true if value "value" is present within set "set".
290 *
291 * @param value a value of any type
292 * @param set an Object which we use to mimic set semantics
293 * (this should make isValueInSet faster than if we used an Array)
294 */
295 $scope.isValueInSet = function(value, set) {
296 return (true == set[value]);
297 }
298
299 /**
300 * If value "value" is already in set "set", remove it; otherwise, add it.
301 *
302 * @param value a value of any type
303 * @param set an Object which we use to mimic set semantics
304 */
305 $scope.toggleValueInSet = function(value, set) {
306 if (true == set[value]) {
307 delete set[value];
308 } else {
309 set[value] = true;
310 }
311 }
312
313
314 //
315 // Array operations; similar to our Set operations, but operate on a
316 // Javascript Array so we *can* easily get a list of all values in the Set.
317 // TODO(epoger): move into a separate .js file?
318 //
319
320 /**
321 * Returns true if value "value" is present within array "array".
322 *
323 * @param value a value of any type
324 * @param array a Javascript Array
325 */
326 $scope.isValueInArray = function(value, array) {
327 return (-1 != array.indexOf(value));
328 }
329
330 /**
331 * If value "value" is already in array "array", remove it; otherwise,
332 * add it.
333 *
334 * @param value a value of any type
335 * @param array a Javascript Array
336 */
337 $scope.toggleValueInArray = function(value, array) {
338 var i = array.indexOf(value);
339 if (-1 == i) {
340 array.push(value);
341 } else {
342 array.splice(i, 1);
343 }
344 }
345
346
347 //
348 // Miscellaneous utility functions.
349 // TODO(epoger): move into a separate .js file?
350 //
351
352 /**
353 * Returns a human-readable (in local time zone) time string for a
354 * particular moment in time.
355 *
356 * @param secondsPastEpoch (numeric): seconds past epoch in UTC
357 */
358 $scope.localTimeString = function(secondsPastEpoch) {
359 var d = new Date(secondsPastEpoch * 1000);
360 return d.toString();
361 }
362
epoger@google.comf9d134d2013-09-27 15:02:44 +0000363 }
364);