github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/public/static/js/task_history.js (about) 1 mciModule.factory('taskHistoryFilter', function($http, $window, $filter) { 2 var ret = {}; 3 4 /* Getter/setter wrapper around the URL hash */ 5 ret.locationHash = { 6 get: function() { 7 var hash = $window.location.hash.substr(1); // Get rid of leading '#' 8 if (hash.charAt(0) == '/') { 9 hash = hash.substr(1); 10 } 11 12 return hash; 13 }, 14 set: function(v) { 15 $window.location.hash = v; 16 } 17 }; 18 19 /* Convert `ret.filter` to a readable string and back for use in the location 20 * hash. */ 21 var filterSerializer = { 22 // Converts `ret.filter` into a readable string 23 serialize: function() { 24 var str = ''; 25 _.each(ret.filter.tests, function(testResult, testName) { 26 if (str.length > 0) { 27 str += '&'; 28 } 29 30 str += encodeURIComponent(testName) + '=' + 31 encodeURIComponent(testResult); 32 }); 33 34 if (str.length > 0) { 35 str += '&'; 36 } 37 38 /* buildVariants is a single string delimited by ',' and each buildVariant 39 * gets piped through encodeURIComponent */ 40 str += 'buildVariants=' + 41 _.map(ret.filter.buildVariants, encodeURIComponent).join(','); 42 43 return str; 44 }, 45 /* The inverse of `serialize`. Takes a string, parses it, and sets 46 * `ret.filter` to the parsed value. */ 47 deserialize: function(str) { 48 ret.filter = ret.filter || {}; 49 ret.filter.tests = {}; 50 ret.filter.buildVariants = []; 51 52 var nameValuePairs = str.split('&'); 53 var testNamesToResults = _.initial(nameValuePairs); 54 _.each(testNamesToResults, function(v) { 55 var nameValuePair = v.split('='); 56 ret.filter.tests[decodeURIComponent(nameValuePair[0])] = 57 decodeURIComponent(nameValuePair[1]); 58 }); 59 60 if (nameValuePairs.length > 0) { 61 var lastNameValuePair = _.last(nameValuePairs).split('='); 62 if (lastNameValuePair[0] === 'buildVariants') { 63 /* If buildVariants isn't empty, split buildVariants by ',' delimiter 64 * and do a URI decode on each of them */ 65 ret.filter.buildVariants = lastNameValuePair[1] ? 66 _.map(lastNameValuePair[1].split(','), decodeURIComponent) : []; 67 } else { 68 ret.filter.tests[decodeURIComponent(lastNameValuePair[0])] = 69 decodeURIComponent(lastNameValuePair[1]); 70 } 71 } 72 } 73 }; 74 75 ret.init = function(buildVariants, taskName, project) { 76 // All build variants 77 ret.buildVariants = buildVariants; 78 ret.taskName = taskName; 79 ret.testNames = []; 80 ret.project = project; 81 82 $http.get( 83 '/task_history/' + 84 encodeURIComponent(ret.project) + 85 '/' + 86 encodeURIComponent(ret.taskName) + 87 '/test_names' 88 ).success(function(testNames) { 89 ret.testNames = []; 90 var testNamesMap = {}; 91 _.each(testNames, function(name) { 92 testNamesMap[$filter('endOfPath')(name)] = true; 93 }); 94 95 _.each(testNamesMap, function(value, key) { 96 ret.testNames.push(key); 97 }); 98 }).error(function(data, status, headers, config) { 99 console.log("Error occurred when getting test names: `" + headers + "`"); 100 }); 101 102 ret.constraints = { 103 low: Number.POSITIVE_INFINITY, 104 high: 0 105 }; 106 107 // Build Variant autocomplete state 108 ret.buildVariantSearchString = ""; 109 ret.buildVariantSearchResults = []; 110 ret.buildVariantSearchDisplay = false; 111 112 // Test results autocomplete state 113 ret.testsSearchString = ""; 114 ret.testsSearchResults = []; 115 ret.testsSearchDisplay = false; 116 117 ret.testsLoading = false; 118 ret.taskMatchesFilter = {}; 119 120 if ($window.location.hash) { 121 filterSerializer.deserialize(ret.locationHash.get()); 122 ret.filter.tests = ret.filter.tests || {}; 123 ret.filter.buildVariants = ret.filter.buildVariants || []; 124 } else { 125 ret.filter = { 126 tests: {}, 127 buildVariants: [] 128 }; 129 } 130 }; 131 132 // Search through provided build variants' names. Used for the build variants 133 // autocomplete 134 ret.searchBuildVariants = function() { 135 ret.buildVariantSearchResults = []; 136 137 if (!ret.buildVariantSearchString) { 138 return; 139 } 140 141 for (var i = 0; i < ret.buildVariants.length; ++i) { 142 if (ret.buildVariants[i].toLowerCase().indexOf(ret.buildVariantSearchString.toLowerCase()) != -1) { 143 ret.buildVariantSearchResults.push(ret.buildVariants[i]); 144 } 145 } 146 147 ret.buildVariantSearchDisplay = true; 148 }; 149 150 // Add a build variant to the filter 151 ret.filterBuildVariant = function(buildVariant) { 152 ret.filter.buildVariants.push(buildVariant); 153 ret.buildVariantSearchString = ""; 154 ret.hideBuildVariantResults(); 155 ret.setLocationHash(); 156 157 // May need to query server again 158 ret.queryServer(); 159 }; 160 161 // Remove the build variant at `index` from the filter 162 ret.removeBuildVariant = function(index) { 163 ret.filter.buildVariants.splice(index, 1); 164 ret.queryServer(); 165 ret.buildVariantSearchString = ""; 166 ret.setLocationHash(); 167 }; 168 169 // Show the autocomplete build variant results 170 ret.showBuildVariantResults = function() { 171 ret.buildVariantSearchDisplay = true; 172 }; 173 174 // Hide the build variant autocomplete results 175 ret.hideBuildVariantResults = function() { 176 ret.buildVariantSearchDisplay = false; 177 }; 178 179 // Search through test names 180 ret.searchTestNames = function() { 181 ret.testsSearchResults = []; 182 183 if (!ret.testsSearchString) { 184 return; 185 } 186 187 for (var i = 0; i < ret.testNames.length; ++i) { 188 if (ret.testNames[i].toLowerCase().indexOf(ret.testsSearchString.toLowerCase()) != -1) { 189 ret.testsSearchResults.push(ret.testNames[i]); 190 } 191 } 192 193 ret.testsSearchDisplay = true; 194 }; 195 196 // Filter for tasks where a test with the given name has a particular result 197 ret.filterTest = function(name, result) { 198 ret.filter.tests[name] = result; 199 ret.testsSearchString = ""; 200 201 ret.queryServer(); 202 203 ret.setLocationHash(); 204 }; 205 206 // Remove a test with a given name from the filter, inverse of above 207 ret.removeTestFilter = function(name) { 208 delete ret.filter.tests[name]; 209 ret.queryServer(); 210 211 ret.setLocationHash(); 212 }; 213 214 // Refresh the location 215 ret.setLocationHash = function() { 216 ret.locationHash.set(filterSerializer.serialize()); 217 }; 218 219 // Given the filter and low/high constraints, ask the server to find which 220 // tasks match the given filter 221 ret.queryServer = function() { 222 ret.testsLoading = true; 223 224 var filterStr = JSON.stringify(ret.filter); 225 var uriFilterStr = encodeURIComponent(filterStr); 226 $http.get( 227 "/task_history/" + 228 encodeURIComponent(ret.project) + 229 '/' + 230 encodeURIComponent(ret.taskName) + 231 "/pickaxe" + 232 "?low=" + ret.constraints.low + 233 "&high=" + ret.constraints.high + 234 "&only_matching_tasks=true" + 235 "&filter=" + uriFilterStr 236 ).success(function(tasks) { 237 ret.testsLoading = false; 238 ret.taskMatchesFilter = {}; 239 if (tasks.length) { 240 tasks.forEach(function(task) { 241 ret.taskMatchesFilter[task.id] = true; 242 }); 243 } 244 }).error(function(data, status, headers, config) { 245 ret.testsLoading = false; 246 console.log("Error occurred when filtering tasks: `" + headers + "`"); 247 }); 248 }; 249 250 // Show the test name autocomplete results 251 ret.showTestNameResults = function() { 252 ret.testsSearchDisplay = true; 253 }; 254 255 // Hide the test name autocomplete results 256 ret.hideTestNameResults = function() { 257 ret.testsSearchDisplay = false; 258 }; 259 260 return ret; 261 }); 262 263 mciModule.controller('TaskHistoryController', function($scope, $window, $http, 264 $filter, $timeout, taskHistoryFilter, mciTaskHistoryRestService, notificationService) { 265 $scope.taskName = $window.taskName; 266 $scope.variants = $window.variants; 267 $scope.versions = []; 268 $scope.failedTestsByTaskId = []; 269 $scope.versionsByGitspec = {}; 270 $scope.tasksByVariantByCommit = []; 271 $scope.testNames = {}; 272 $scope.taskHistoryFilter = taskHistoryFilter; 273 $scope.isTaskGroupInactive = {}; 274 $scope.inactiveTaskGroupCount = {}; 275 $scope.exhaustedBefore = $window.exhaustedBefore; 276 $scope.exhaustedAfter = $window.exhaustedAfter; 277 $scope.selectedRevision = $window.selectedRevision; 278 279 $scope.init = function(project) { 280 $scope.project = project; 281 $scope.taskHistoryFilter.init($scope.variants, $scope.taskName, project); 282 283 /* Populate initial page data */ 284 buildVersionsByRevisionMap($window.versions, true); 285 $scope.failedTestsByTaskId = $window.failedTasks; 286 buildTasksByVariantCommitMap($window.tasksByCommit, true); 287 288 var numVersions = $scope.versions.length; 289 if (numVersions > 0) { 290 $scope.firstVersion = $scope.versions[0].revision; 291 $scope.lastVersion = $scope.versions[numVersions - 1].revision; 292 } 293 }; 294 295 function buildVersionsByRevisionMap(versions, before) { 296 for (var i = 0; i < versions.length; ++i) { 297 $scope.versionsByGitspec[versions[i].revision] = versions[i]; 298 299 if (versions[i].order > $scope.taskHistoryFilter.constraints.high) { 300 $scope.taskHistoryFilter.constraints.high = versions[i].order; 301 } 302 if (versions[i].order < $scope.taskHistoryFilter.constraints.low) { 303 $scope.taskHistoryFilter.constraints.low = versions[i].order; 304 } 305 } 306 307 if (before) { 308 Array.prototype.push.apply($scope.versions, versions); 309 } else { 310 Array.prototype.unshift.apply($scope.versions, versions); 311 } 312 313 // Make sure our filter gets updated against the server, because high 314 // and low may have changed 315 $scope.taskHistoryFilter.queryServer(); 316 } 317 318 function buildTasksByVariantCommitMap(tasksByCommit, before) { 319 $scope.testNames = {}; 320 321 if (!tasksByCommit || !tasksByCommit.length) { 322 return; 323 } 324 325 $scope.isTaskGroupInactive = {}; 326 $scope.inactiveTaskGroupCount = {}; 327 328 var tasksByVariant = []; 329 for (var i = 0; i < tasksByCommit.length; ++i) { 330 var commitTasks = tasksByCommit[i]; 331 var buildVariantTaskMap = {}; 332 for (var j = 0; j < commitTasks.tasks.length; ++j) { 333 buildVariantTaskMap[commitTasks.tasks[j].build_variant] = 334 commitTasks.tasks[j]; 335 } 336 337 tasksByVariant.push({ 338 _id: commitTasks._id, 339 tasksByVariant: buildVariantTaskMap 340 }); 341 } 342 343 if (before) { 344 Array.prototype.push.apply($scope.tasksByVariantByCommit, tasksByVariant); 345 } else { 346 Array.prototype.unshift.apply($scope.tasksByVariantByCommit, tasksByVariant); 347 } 348 349 350 var inactiveVersionSequenceStart = -1; 351 _.each($scope.tasksByVariantByCommit, function(obj, index) { 352 $scope.isTaskGroupInactive[obj._id] = isTaskGroupInactive($scope.variants, obj); 353 if ($scope.isTaskGroupInactive[obj._id]) { 354 if (inactiveVersionSequenceStart == -1) { 355 inactiveVersionSequenceStart = index; 356 $scope.inactiveTaskGroupCount[inactiveVersionSequenceStart] = 0; 357 } 358 ++$scope.inactiveTaskGroupCount[inactiveVersionSequenceStart]; 359 } else { 360 inactiveVersionSequenceStart = -1; 361 } 362 }); 363 364 } 365 366 $scope.taskMatchesFilter = function(task) { 367 filter = $scope.taskHistoryFilter.filter; 368 if (filter.buildVariants.length > 0) { 369 if (filter.buildVariants.indexOf(task.build_variant) == -1) { 370 return false; 371 } 372 } 373 374 if (filter.tests && !_.isEmpty(filter.tests) && task) { 375 return $scope.taskHistoryFilter.taskMatchesFilter[task._id]; 376 } 377 378 return true; 379 }; 380 381 382 $scope.variantInFilter = function(variant) { 383 filter = $scope.taskHistoryFilter.filter; 384 if (filter.buildVariants.length > 0) { 385 return filter.buildVariants.indexOf(variant) != -1; 386 } 387 388 return true; 389 }; 390 391 $scope.taskGroupHasTaskMatchingFilter = function(variants, taskGroup) { 392 for (var i = 0; i < variants.length; ++i) { 393 var variant = variants[i]; 394 if (taskGroup.tasksByVariant[variant] && 395 $scope.taskMatchesFilter(taskGroup.tasksByVariant[variant])) { 396 return true; 397 } 398 } 399 400 return false; 401 }; 402 403 var isTaskGroupInactive = function(variants, taskGroup) { 404 for (var i = 0; i < variants.length; ++i) { 405 var task = taskGroup.tasksByVariant[variants[i]]; 406 if (task && ['success', 'failed'].indexOf(task.status) != -1) { 407 return false; 408 } 409 } 410 411 return true; 412 }; 413 414 $scope.getTestForVariant = function(testGroup, buildvariant) { 415 return testGroup.tasksByVariant[buildvariant]; 416 } 417 418 $scope.getVersionForCommit = function(gitspec) { 419 return $scope.versionsByGitspec[gitspec]; 420 } 421 422 $scope.getTaskTooltip = function(testGroup, buildvariant) { 423 var task = testGroup.tasksByVariant[buildvariant]; 424 var tooltip = ''; 425 if (!task) { 426 return 427 } 428 429 switch (task.status) { 430 case 'failed': 431 if ('task_end_details' in task && 'timed_out' in task.task_end_details && task.task_end_details.timed_out) { 432 tooltip += 'Timed out (' + task.task_end_details.desc + ') in ' + 433 $filter('stringifyNanoseconds')(task.time_taken); 434 } else if (task._id in $scope.failedTestsByTaskId && 435 $scope.failedTestsByTaskId[task._id].length > 0) { 436 var failedTests = $scope.failedTestsByTaskId[task._id]; 437 var failedTestLimit = 3; 438 var displayedTests = []; 439 for (var i = 0; i < failedTests.length; i++) { 440 if (i < failedTestLimit) { 441 displayedTests.push($filter('endOfPath')(failedTests[i].test_file)); 442 } 443 } 444 tooltip += failedTests.length + ' ' + $filter('pluralize')(failedTests.length, 'test') + 445 ' failed (' + $filter('stringifyNanoseconds')(task.time_taken) + ')\n'; 446 _.each(displayedTests, function(displayedTest) { 447 tooltip += '- ' + displayedTest + '\n'; 448 }); 449 } else { 450 tooltip = $filter('capitalize')(task.status) + ' (' + 451 $filter('stringifyNanoseconds')(task.time_taken) + ')'; 452 } 453 break; 454 case 'success': 455 tooltip = $filter('capitalize')(task.status) + ' (' + 456 $filter('stringifyNanoseconds')(task.time_taken) + ')'; 457 break; 458 default: 459 } 460 return tooltip; 461 } 462 463 $scope.getGridClass = function(cell) { 464 if (cell) { 465 if (cell.status == 'undispatched') { 466 return 'undispatched ' + (cell.activated ? 'active' : 'inactive') 467 } 468 cell.task_end_details = cell.details; 469 return $filter('statusFilter')(cell); 470 } 471 return 'skipped'; 472 }; 473 474 475 $scope.loadMore = function(before) { 476 $scope.taskHistoryFilter.testsLoading = true; 477 var revision = $scope.firstVersion; 478 if (before) { 479 revision = $scope.lastVersion; 480 } 481 482 mciTaskHistoryRestService.getTaskHistory( 483 $scope.project, 484 $scope.taskName, { 485 format: 'json', 486 revision: revision, 487 before: before, 488 }, { 489 success: function(data, status) { 490 if (data.Versions) { 491 buildVersionsByRevisionMap(data.Versions, before); 492 493 var numVersions = data.Versions.length; 494 if (numVersions > 0) { 495 if (before) { 496 $scope.lastVersion = data.Versions[numVersions - 1].revision; 497 $scope.exhaustedBefore = data.ExhaustedBefore; 498 } else { 499 $scope.firstVersion = data.Versions[0].revision; 500 $scope.exhaustedAfter = data.ExhaustedAfter; 501 } 502 } 503 } 504 505 if (data.Tasks) { 506 buildTasksByVariantCommitMap(data.Tasks, before) 507 } 508 509 // add column highlighting to the new elements. use a $timeout to 510 // let the new elements get created before the handlers are registered 511 $timeout(function() { 512 $window.addColumnHighlighting(true); 513 }, 0); 514 }, 515 516 error: function(jqXHR, status, errorThrown) { 517 notificationService.pushNotification('Error getting task history: ' + jqXHR,'errorNearButton'); 518 } 519 } 520 ); 521 }; 522 523 $scope.hideInactiveVersions = { 524 v: true, 525 get: function() { 526 return $scope.hideInactiveVersions.v; 527 }, 528 toggle: function() { 529 $scope.hideInactiveVersions.v = !$scope.hideInactiveVersions.v; 530 } 531 }; 532 533 $scope.hideUnmatchingVersions = { 534 v: false, 535 get: function() { 536 return $scope.hideUnmatchingVersions.v; 537 }, 538 toggle: function() { 539 $scope.hideUnmatchingVersions.v = !$scope.hideUnmatchingVersions.v; 540 }, 541 hidden: function(){ 542 return _.size($scope.taskHistoryFilter.filter.tests) == 0 543 } 544 }; 545 }); 546 547 548 549 // function to add mouseover handlers for highlighting columns 550 function addColumnHighlighting(unbindPrevious) { 551 $("div[class*='column-']").each(function(i, el) { 552 var elClasses = $(el).attr("class").split(' '); 553 var columnClass = null; 554 _.each(elClasses, function(c) { 555 if (c.indexOf('column-') === 0) { 556 columnClass = c; 557 } 558 }); 559 560 if (!columnClass) { 561 return; 562 } 563 564 // this is a little aggressive, but since we don't attach any 565 // other handlers for these events anywhere it should be okay 566 if (unbindPrevious) { 567 $(el).off('mouseenter'); 568 $(el).off('mouseleave'); 569 } 570 571 $(el).on("mouseenter", function() { 572 $('.' + columnClass).addClass('highlight-column'); 573 }); 574 575 $(el).on("mouseleave", function() { 576 $('.' + columnClass).removeClass('highlight-column'); 577 }); 578 }); 579 }; 580 581 // add column highlighting on document ready 582 $(document).ready(function() { 583 addColumnHighlighting(false); 584 });