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  });