github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/public/static/js/task.js (about) 1 mciModule.controller('TaskHistoryDrawerCtrl', function($scope, $window, $location, $filter, $timeout, historyDrawerService) { 2 3 // cache the task being displayed on the page 4 $scope.task = $window.task_data; 5 6 // cache the element for the content of the drawer 7 var drawerContentsEl = $('#drawer-contents'); 8 9 // is the specified revision the one with the current task in it? 10 $scope.isCurrent = function(revision) { 11 return revision.revision === $scope.task.gitspec; 12 } 13 14 // helper to convert the history fetched from the backend into revisions, 15 // grouped by date, for front-end display 16 function groupHistory(history) { 17 18 // group the revisions by date, ordered backwards by date 19 var groupedRevisions = []; 20 var datesSeen = {}; // to avoid double-entering dates 21 history.forEach(function(revision) { 22 var date = revision.push_time.substring(0, 10); 23 24 // if we haven't seen the date, add a new entry for it 25 if (!datesSeen[date]) { 26 groupedRevisions.push({ 27 date: date, 28 revisions: [], 29 }); 30 datesSeen[date] = true; 31 } 32 33 // push the revision onto its appropriate date group (always the 34 // last in the list) 35 groupedRevisions[groupedRevisions.length - 1].revisions.push(revision); 36 }); 37 38 return groupedRevisions; 39 40 } 41 42 // make a backend call to get the drawer contents 43 function fetchHistory() { 44 historyDrawerService.fetchTaskHistory($scope.task.id, 'surround', 20, { 45 success: function(data) { 46 47 // save the revisions as a list 48 $scope.revisions = data.revisions; 49 50 // group the history by revision, and save it 51 $scope.groupedRevisions = groupHistory(data.revisions); 52 53 // scroll to the relevant element 54 $timeout( 55 function() { 56 var currentRevisionDomEl = $('.drawer-item-highlighted')[0]; 57 if (!currentRevisionDomEl) { 58 return; 59 } 60 var offsetTop = $(currentRevisionDomEl).position().top; 61 var drawerContentsHeight = drawerContentsEl.height(); 62 if (offsetTop >= drawerContentsHeight) { 63 drawerContentsEl.scrollTop(offsetTop); 64 } 65 }, 500) 66 }, 67 error: function(data) { 68 console.log('error fetching history: ' + data); 69 } 70 }); 71 72 } 73 74 // function fired when scrolling up hits the top of the frame, 75 // loads more revisions asynchronously 76 var fetchLaterRevisions = _.debounce( 77 function() { 78 // get the most recent revision in the history 79 var mostRecentRevision = ($scope.revisions && $scope.revisions[0]); 80 81 // no history 82 if (!mostRecentRevision) { 83 return 84 } 85 86 // get a task id from it 87 var anchorId = mostRecentRevision.task.id; 88 89 historyDrawerService.fetchTaskHistory(anchorId, 'after', 20, { 90 success: function(data) { 91 // no computation necessary 92 if (!data) { 93 return 94 } 95 96 // place on the beginning of the stored revisions 97 $scope.revisions = data.revisions.concat($scope.revisions); 98 99 // regroup 100 $scope.groupedRevisions = groupHistory($scope.revisions); 101 102 }, 103 error: function(data) { 104 console.log('error fetching later revisions: ' + data); 105 } 106 }) 107 }, 500, true); 108 109 // function fired when scrolling down hits the bottom of the frame, 110 // loads more revisions asynchronously 111 var fetchEarlierRevisions = _.debounce( 112 113 function() { 114 // get the least recent revision in the history 115 var leastRecentRevision = ($scope.revisions && 116 $scope.revisions[$scope.revisions.length - 1]); 117 118 // no history 119 if (!leastRecentRevision) { 120 return 121 } 122 123 // get a task id from it 124 var anchorId = leastRecentRevision.task.id; 125 126 historyDrawerService.fetchTaskHistory( 127 anchorId, 128 'before', 129 20, { 130 success: function(data) { 131 // no computation necessary 132 if (!data) { 133 return 134 } 135 136 // place on the end of the stored revisions 137 $scope.revisions = $scope.revisions.concat(data.revisions); 138 139 // regroup 140 $scope.groupedRevisions = groupHistory($scope.revisions); 141 142 }, 143 error: function(data) { 144 console.log('error fetching earlier revisions: ' + data); 145 } 146 } 147 ) 148 149 }, 150 500, 151 true 152 ); 153 154 // make the initial call to fetch the history 155 fetchHistory(); 156 157 /* infinite scroll stuff */ 158 159 // the drawer header element 160 var drawerHeaderEl = $('#drawer-header'); 161 162 // the filled part of the drawer 163 var drawerFilledEl = $('#drawer-filled'); 164 165 // scrolling function to fire if the element is not actually scrollable 166 // (does not overflow its div) 167 var loadMoreSmall = _.debounce( 168 function(e) { 169 var evt = window.event || e; 170 if (evt.wheelDelta) { 171 if (evt.wheelDelta < 0) { 172 fetchEarlierRevisions(); 173 } else { 174 fetchLaterRevisions(); 175 } 176 } 177 178 // firefox 179 else if (evt.detail && evt.detail.wheelDelta) { 180 if (evt.detail.wheelDelta < 0) { 181 fetchLaterRevisions(); 182 } else { 183 fetchEarlierRevisions(); 184 } 185 } 186 }, 187 500, 188 true 189 ); 190 191 // activates infinite scrolling if the drawer contents are not large enough 192 // to be normally scrollable 193 var smallScrollFunc = function(e) { 194 if (drawerFilledEl.height() < drawerContentsEl.height()) { 195 loadMoreSmall(e); 196 } 197 } 198 199 drawerContentsEl.on('mousewheel DOMMouseScroll onmousewheel', smallScrollFunc); 200 201 // scrolling function to fire if the element is scrollable (it overflows 202 // its div) 203 var bigScrollFunc = function() { 204 if (drawerContentsEl.scrollTop() === 0) { 205 // we hit the top of the drawer 206 fetchLaterRevisions(); 207 208 } else if (drawerContentsEl.scrollTop() + 10 >= 209 drawerContentsEl[0].scrollHeight - drawerContentsEl.height()) { 210 211 // we hit the bottom of the drawer 212 fetchEarlierRevisions(); 213 214 } 215 } 216 217 // set up infinite scrolling on the drawer element 218 drawerContentsEl.scroll(bigScrollFunc); 219 220 var eopFilter = $filter('endOfPath'); 221 $scope.failuresTooltip = function(failures) { 222 return _.map(failures, function(failure) { 223 return eopFilter(failure); 224 }).join('\n'); 225 } 226 227 }); 228 229 mciModule.controller('TaskCtrl', function($scope, $rootScope, $now, $timeout, $interval, md5, $filter, $window, $http, $locationHash) { 230 $scope.userTz = $window.userTz; 231 $scope.haveUser = $window.have_user; 232 $scope.taskHost = $window.taskHost; 233 234 // Returns true if 'testResult' represents a test failure, and returns false otherwise. 235 $scope.hasTestFailureStatus = function hasTestFailureStatus(testResult) { 236 var failureStatuses = ['fail', 'silentfail']; 237 return failureStatuses.indexOf(testResult.status) >= 0; 238 }; 239 240 $scope.getURL = function(testResult, isRaw) { 241 var url = (isRaw) ? testResult.url_raw : testResult.url; 242 243 if (url != '') { 244 return url; 245 } 246 247 var logid = testResult.log_id; 248 var linenum = testResult.line_num || 0; 249 250 url = '/test_log/' + logid + '#L' + linenum; 251 if (isRaw) { 252 url = '/test_log/' + logid + '?raw=1'; 253 } 254 255 return url; 256 }; 257 258 $scope.hideURL = function(testResult, isRaw) { 259 var url = isRaw ? testResult.url_raw : testResult.url; 260 return !((url != '') || (testResult.log_id)); 261 }; 262 263 $scope.hasBothURL = function(testResult) { 264 return !($scope.hideURL(testResult) || $scope.hideURL(testResult,'raw')) 265 }; 266 267 var hash = $locationHash.get(); 268 $scope.hash = hash; 269 270 $scope.getSpawnLink = function(){ 271 if(!$scope.haveUser) { // user is not logged in, so we won't provide a link. 272 return "" 273 } 274 if(!$scope.taskHost || $scope.taskHost.distro.provider == "static" || !$scope.taskHost.distro.spawn_allowed){ 275 return "" 276 } 277 return "/spawn?distro_id=" + $scope.taskHost.distro._id + "&task_id=" + $scope.task.id 278 } 279 280 $scope.setSortBy = function(order) { 281 $scope.sortBy = order; 282 hash.sort = order.name; 283 $locationHash.set(hash); 284 }; 285 286 $scope.linkToTest = function(testName) { 287 if (hash.test === testName) { 288 delete hash.test; 289 $locationHash.set(hash); 290 } else { 291 hash.test = testName; 292 $locationHash.set(hash); 293 } 294 }; 295 296 297 $scope.setTask = function(task) { 298 $scope.task = task; 299 $scope.md5 = md5; 300 $scope.maxTests = 1; 301 302 /** 303 * Defines the sort order for a test's status. 304 */ 305 function ordinalForTestStatus(testResult) { 306 var orderedTestStatuses = ['fail', 'silentfail', 'pass', 'skip']; 307 return orderedTestStatuses.indexOf(testResult.status); 308 } 309 310 $scope.sortOrders = [{ 311 name: 'Status', 312 by: [ordinalForTestStatus, 'display_name'], 313 reverse: false 314 }, { 315 name: 'Name', 316 by: ['display_name'], 317 reverse: false 318 }, { 319 name: 'Time Taken', 320 by: ['time_taken', 'display_name'], 321 reverse: true 322 }, { 323 name: 'Sequence', 324 by: [''], 325 reverse: true 326 }]; 327 328 var totalTestTime = 0; 329 (task.test_results || []).forEach(function(testResult) { 330 testResult.time_taken = testResult.end - testResult.start; 331 totalTestTime += testResult.time_taken; 332 testResult.display_name = $filter('endOfPath')(testResult.test_file); 333 }); 334 $scope.totalTestTimeNano = totalTestTime * 1000 * 1000 * 1000; 335 336 if (hash.sort) { 337 var index = _.indexOf(_.pluck($scope.sortOrders, 'name'), hash.sort); 338 if (index != -1) { 339 $scope.sortBy = $scope.sortOrders[index]; 340 } 341 } 342 343 if (task.execution > 0 || task.archived) { 344 $scope.otherExecutions = _.range(task.total_executions + 1) 345 } 346 347 $scope.sortBy = $scope.sortOrders[0]; 348 349 $scope.isMet = function(dependency) { 350 // check if a dependency is met, unmet, or in progress 351 if (dependency.task_waiting == "blocked") { 352 return "unmet"; 353 } 354 if (dependency.status != "failed" && dependency.status != "success") { 355 // if we didn't succeed or fail, don't report anything 356 return ""; 357 } 358 if (dependency.status == dependency.required || dependency.required == "*") { 359 return "met"; 360 } 361 return "unmet"; 362 }; 363 364 $scope.timeTaken = $scope.task.time_taken 365 366 if ($scope.task.patch_info) { 367 $scope.baseTimeTaken = $scope.task.patch_info.base_time_taken; 368 } 369 370 if ($scope.task.status != 'failed' && $scope.task.status != 'success') { 371 updateFunc = function() { 372 $scope.task.current_time += 1000000000; // 1 second 373 $scope.timeTaken = $scope.task.current_time - $scope.task.start_time; 374 $scope.timeToCompletion = $scope.task.expected_duration - ($scope.task.current_time - $scope.task.start_time); 375 if ($scope.timeToCompletion < 0) { 376 $scope.timeToCompletion = 'unknown'; 377 } 378 if ($scope.task.status === 'undispatched'){ 379 $scope.timeToCompletion = $scope.task.expected_duration; 380 } 381 } 382 updateFunc(); 383 var updateTimers = $interval(updateFunc, 1000); 384 } 385 }; 386 387 $rootScope.$on("task_updated", function(e, newTask){ 388 newTask.version_id = $scope.task.version_id; 389 newTask.message = $scope.task.message; 390 newTask.author = $scope.task.author; 391 newTask.author_email = $scope.task.author_email; 392 newTask.min_queue_pos = $scope.task.min_queue_pos; 393 newTask.patch_info = $scope.task.patch_info; 394 newTask.build_variant_display = $scope.task.build_variant_display; 395 newTask.depends_on = $scope.task.depends_on; 396 $scope.setTask(newTask); 397 }) 398 399 $scope.setTask($window.task_data); 400 $scope.plugins = $window.plugins 401 402 $scope.maxTestTime = 1; 403 404 $scope.lastUpdate = $now.now(); 405 406 $scope.githubLink = function() { 407 if (!$scope.task) { 408 return '#'; 409 } 410 var projectComponents = $scope.task.project.split('-'); 411 if (projectComponents[projectComponents.length - 1] === 'master') { 412 projectComponents = projectComponents.slice(0, projectComponents.length - 1); 413 } 414 return '//github.com/' + projectComponents.join('/') + '/commit/' + $scope.task.gitspec; 415 }; 416 417 // Returns URL to task history page with a filter on the particular test 418 // and test status enabled. 419 $scope.getTestHistoryUrl = function(project, task, test) { 420 var url = '/task_history/' + 421 encodeURIComponent(project) + '/' + 422 encodeURIComponent(task.display_name) + '?revision=' + 423 encodeURIComponent(task.gitspec); 424 if (test) { 425 url += '#' + encodeURIComponent(test.display_name) + '=' + 426 encodeURIComponent(test.status); 427 } 428 return url 429 }; 430 431 }); 432 433 mciModule.directive('testsResultsBar', function($filter) { 434 return { 435 scope: true, 436 link: function(scope, element, attrs) { 437 scope.$watch(attrs.testsResultsBar, function(testResults) { 438 if (testResults) { 439 var numSuccess = 0; 440 var numFailed = 0; 441 var successTimeTaken = 0; 442 var failureTimeTaken = 0; 443 _.each(testResults, function(result) { 444 switch (result.status) { 445 case 'pass': 446 numSuccess++; 447 successTimeTaken += (result.end - result.start); 448 break; 449 case 'fail': 450 case 'silentfail': 451 numFailed++; 452 failureTimeTaken += (result.end - result.start); 453 break; 454 } 455 }); 456 457 successTimeTaken = $filter('nanoToSeconds')(successTimeTaken) 458 failureTimeTaken = $filter('nanoToSeconds')(failureTimeTaken) 459 var successTitle = numSuccess + ' test' + (numSuccess == 1 ? '' : 's') + ' succeeded in ' + $filter('stringifyNanoseconds')(successTimeTaken); 460 var failedTitle = numFailed + ' test' + (numFailed == 1 ? '' : 's') + ' failed in ' + $filter('stringifyNanoseconds')(failureTimeTaken); 461 element.html('<div class="progress-bar progress-bar-success" role="progressbar" style="width: ' + (numSuccess / testResults.length * 100) + '%" data-animation="" data-toggle="tooltip" title="' + successTitle + '"></div>' + 462 '<div class="progress-bar progress-bar-danger" role="progressbar" style="width: ' + (numFailed / testResults.length * 100) + '%" data-animation="" data-toggle="tooltip" title="' + failedTitle + '"></div>') 463 464 $(element.children('*[data-toggle="tooltip"]')).each(function(i, el) { 465 $(el).tooltip(); 466 }); 467 468 scope.barWidth = testResults.length / scope.maxTests * 90; 469 } 470 }); 471 } 472 } 473 }); 474 475 mciModule.directive('testResults', function() { 476 return { 477 scope: true, 478 link: function(scope, element, attrs) { 479 scope.maxTestTime = 1; 480 scope.$watch(attrs.testResults, function(testResults) { 481 if (testResults) { 482 for (var i = 0; i < testResults.length; i++) { 483 var timeTaken = testResults[i].end - testResults[i].start; 484 if (scope.maxTestTime < timeTaken) { 485 scope.maxTestTime = timeTaken; 486 } 487 } 488 } 489 }); 490 } 491 } 492 }); 493 494 mciModule.directive('testResultBar', function($filter) { 495 return { 496 scope: true, 497 link: function(scope, element, attrs) { 498 scope.$watch(attrs.testResultBar, function(testResult) { 499 var timeTaken = testResult.end - testResult.start; 500 scope.timeTaken = timeTaken; 501 502 switch (testResult.status) { 503 case 'pass': 504 scope.progressBarClass = 'progress-bar-success'; 505 break; 506 case 'fail': 507 scope.progressBarClass = 'progress-bar-danger'; 508 break; 509 case 'silentfail': 510 scope.progressBarClass = 'progress-bar-silently-failed'; 511 break; 512 default: 513 scope.progressBarClass = 'progress-bar-default'; 514 } 515 516 var timeInNano = scope.timeTaken * 1000 * 1000 * 1000; 517 $(element).tooltip({ 518 title: $filter('stringifyNanoseconds')(timeInNano), 519 animation: false, 520 }); 521 }); 522 523 scope.$watch('maxTestTime', function(maxTestTime) { 524 scope.barWidth = scope.timeTaken / maxTestTime * 90; 525 if (scope.barWidth < 5) { 526 scope.barWidth = 5; 527 } 528 }); 529 } 530 } 531 }); 532 533 mciModule.controller('TaskLogCtrl', ['$scope', '$timeout', '$http', '$location', '$window', '$filter', 'notificationService', function($scope, $timeout, $http, $location, $window, $filter, notifier) { 534 $scope.logs = 'Loading...'; 535 $scope.task = {}; 536 $scope.eventLogs = 'EV'; 537 $scope.systemLogs = 'S'; 538 $scope.agentLogs = 'E'; 539 $scope.taskLogs = 'T'; 540 $scope.allLogs = 'ALL'; 541 $scope.userTz = $window.userTz; 542 543 var logSpec = $location.path().split('/'); 544 $scope.currentLogs = logSpec[2] || $scope.taskLogs; 545 546 $scope.$watch('currentLogs', function() { 547 $scope.getLogs(); 548 }); 549 550 $scope.setCurrentLogs = function(currentLogs) { 551 $scope.logs = 'Loading...'; 552 $scope.currentLogs = currentLogs; 553 $location.path('log/' + currentLogs); 554 }; 555 556 $scope.formatTimestamp = function(logEntry, minVersion) { 557 if (!logEntry.version || logEntry.version < minVersion) { 558 return ''; 559 } 560 561 var converter = $filter('convertDateToUserTimezone'); 562 var format = 'YYYY/MM/DD HH:mm:ss.SSS'; 563 var timestamp = converter(logEntry.timestamp, $scope.userTz, format); 564 return '[' + timestamp + '] ' 565 } 566 567 $scope.getLogs = function() { 568 $http.get('/json/task_log/' + $scope.taskId + '/' + $scope.task.execution + '?type=' + $scope.currentLogs). 569 success(function(data, status) { 570 if ($scope.currentLogs == $scope.eventLogs) { 571 $scope.eventLogData = data.reverse() 572 } else { 573 if (data && data.LogMessages) { 574 //read the log messages out, and reverse their order (since they are returned backwards) 575 $scope.logs = _.map(data.LogMessages, function(entry) { 576 var msg = entry.m.replace(/"/g, '"'); 577 var date = new Date(entry.ts); 578 return { 579 message: msg, 580 severity: entry.s, 581 timestamp: date, 582 version: entry.v 583 }; 584 }); 585 } else { 586 $scope.logs = []; 587 } 588 } 589 }). 590 error(function(jqXHR, status, errorThrown) { 591 notifier.pushNotification('Error retrieving logs: ' + jqXHR, 'errorHeader'); 592 }); 593 594 // If we already have an outstanding timeout, cancel it 595 if ($scope.getLogsTimeout) { 596 $timeout.cancel($scope.getLogsTimeout); 597 } 598 599 $scope.getLogsTimeout = $timeout(function() { 600 $scope.getLogs(); 601 }, 5000); 602 }; 603 604 $scope.getRawLogLink = function(isRaw) { 605 if ($scope.currentLogs === $scope.eventLogs) { 606 return '/event_log/task/' + $scope.taskId; 607 } else { 608 var raw = isRaw ? '&text=true' : ''; 609 return '/task_log_raw/' + $scope.taskId + '/' + $scope.task.execution + '?type=' + $scope.currentLogs + raw; 610 } 611 }; 612 613 $scope.setTask = function(task) { 614 $scope.task = task; 615 $scope.taskId = task.id; 616 }; 617 618 $scope.setTask($window.task_data); 619 620 }]);