github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/public/static/js/tasks_grid.js (about)

     1  const MaxFailedTestDisplay = 5;
     2  
     3  // endOfPath strips off all of the begging characters from a file path so that just the file name is left.
     4  function endOfPath(input) {
     5    var lastSlash = input.lastIndexOf('/');
     6    if (lastSlash === -1 || lastSlash === input.length - 1) {
     7      // try to find the index using windows-style filesystem separators
     8      lastSlash = input.lastIndexOf('\\');
     9      if (lastSlash === -1 || lastSlash === input.length - 1) {
    10        return input;
    11      }
    12    }
    13    return input.substring(lastSlash + 1);
    14  }
    15  
    16  // taskStatusClass returns the css class that should be associated with a given task so that it can
    17  // be properly styled.
    18  function taskStatusClass(task) {
    19    if (task !== Object(task)) {
    20  	  return '';
    21    }
    22  
    23    if (task.status == 'undispatched') {
    24      if (!task.activated) {
    25        return 'inactive';
    26      } else {
    27        return 'unstarted';
    28      }
    29    }
    30  
    31    if (task.status == 'failed') {
    32      if ('task_end_details' in task) {
    33        if ('type' in task.task_end_details && task.task_end_details.type == 'system') {
    34           return 'system-failed';
    35        }
    36        if (!!task.task_end_details.timed_out && task.task_end_details.desc == 'heartbeat') {
    37          return 'system-failed';
    38        }
    39      }
    40      return 'failed';
    41    }
    42    return task.status;
    43  }
    44  
    45  // labelFromTask returns the human readable label for a task's status given the details of its execution.
    46  function labelFromTask(task){
    47    if (task !== Object(task)) {
    48  	  return '';
    49    }
    50  
    51    if (task.status == 'undispatched') {
    52      if (task.activated) {
    53        if (task.task_waiting) {
    54          return task.task_waiting;
    55        }
    56        return 'scheduled';
    57      } else if (+task.dispatch_time == 0 || (typeof task.dispatch_time == "string" && +new Date(task.dispatch_time) <= 0)) {
    58         return 'not scheduled';
    59      }
    60    }
    61  
    62    if (task.status == 'failed' && 'task_end_details' in task){
    63      if ('timed_out' in task.task_end_details) {
    64        if (task.task_end_details.timed_out && task.task_end_details.desc == 'heartbeat') {
    65          return 'system unresponsive';
    66        }
    67        if (task.task_end_details.type == 'system') {
    68          return 'system timed out';
    69        }
    70        return 'test timed out';
    71      }
    72      if (task.task_end_details.type == 'system') {
    73        return 'system failure';
    74      }
    75    }
    76  
    77    return task.status;
    78  }
    79  
    80  // stringifyNanoseconds takes an integer count of nanoseconds and
    81  // returns it formatted as a human readable string, like "1h32m40s"
    82  // If skipDayMax is true, then durations longer than 1 day will be represented
    83  // in hours. Otherwise, they will be displayed as '>=1 day'
    84  function stringifyNanoseconds(input, skipDayMax, skipSecMax) {
    85    var NS_PER_MS = 1000 * 1000; // 10^6
    86    var NS_PER_SEC = NS_PER_MS * 1000
    87    var NS_PER_MINUTE = NS_PER_SEC * 60;
    88    var NS_PER_HOUR = NS_PER_MINUTE * 60;
    89  
    90    if (input == 0) {
    91      return "0 seconds";
    92    } else if (input < NS_PER_MS) {
    93      return "< 1 ms";
    94    } else if (input < NS_PER_SEC) {
    95      if (skipSecMax){
    96        return Math.floor(input / NS_PER_MS) + " ms";
    97      } else {
    98        return "< 1 second"
    99      }
   100    } else if (input < NS_PER_MINUTE) {
   101      return Math.floor(input / NS_PER_SEC) + " seconds";
   102    } else if (input < NS_PER_HOUR) {
   103      return Math.floor(input / NS_PER_MINUTE) + "m " + Math.floor((input % NS_PER_MINUTE) / NS_PER_SEC) + "s";
   104    } else if (input < NS_PER_HOUR * 24 || skipDayMax) {
   105      return Math.floor(input / NS_PER_HOUR) + "h " +
   106          Math.floor((input % NS_PER_HOUR) / NS_PER_MINUTE) + "m " +
   107          Math.floor((input % NS_PER_MINUTE) / NS_PER_SEC) + "s";
   108    } else if (input == "unknown") {
   109      return "unknown";
   110    }  else {
   111      return ">= 1 day";
   112    }
   113  }
   114  
   115  // Grid
   116  
   117  // The main class that binds to the root div. This contains all the distros, builds, and tasks
   118  function Grid ({data, project, collapseInfo, buildVariantFilter, taskFilter}) {
   119    return (
   120      React.createElement("div", {className: "waterfall-grid"}, 
   121        
   122          data.rows.filter(function(row){
   123            return row.build_variant.display_name.toLowerCase().indexOf(buildVariantFilter.toLowerCase()) != -1;
   124          })
   125          .map(function(row){
   126            return React.createElement(Variant, {row: row, project: project, collapseInfo: collapseInfo, versions: data.versions, taskFilter: taskFilter, currentTime: data.current_time});
   127          })
   128        
   129      ) 
   130    )
   131  };
   132  
   133  function filterActiveTasks(tasks, activeStatuses){
   134    return _.filter(tasks, function(task) { 
   135        return _.contains(activeStatuses, task.status);
   136      });
   137  }
   138  
   139  // The class for each "row" of the waterfall page. Includes the build variant link, as well as the five columns
   140  // of versions.
   141  function Variant({row, versions, project, collapseInfo, taskFilter, currentTime}) {
   142        return (
   143        React.createElement("div", {className: "row variant-row"}, 
   144          React.createElement("div", {className: "col-xs-2 build-variants"}, 
   145            row.build_variant.display_name
   146          ), 
   147          React.createElement("div", {className: "col-xs-10"}, 
   148            React.createElement("div", {className: "row build-cells"}, 
   149              
   150                versions.map(function(version, i){
   151                    return(React.createElement("div", {className: "waterfall-build"}, 
   152                      React.createElement(Build, {key: version.ids[0], 
   153                                  build: row.builds[version.ids[0]], 
   154                                  rolledUp: version.rolled_up, 
   155                                  collapseInfo: collapseInfo, 
   156                                  taskFilter: taskFilter, 
   157                                  currentTime: currentTime})
   158                    )
   159                    );
   160                })
   161              
   162            )
   163          )
   164        )
   165      )
   166  }
   167  
   168  
   169  // Each Build class is one group of tasks for an version + build variant intersection
   170  // We case on whether or not a build is active or not, and return either an ActiveBuild or InactiveBuild respectively
   171  
   172  function Build({build, collapseInfo, rolledUp, taskFilter, currentTime}){
   173    // inactive build
   174    if (rolledUp) {
   175      return React.createElement(InactiveBuild, null);
   176    }
   177  
   178    // no build for this version
   179    if (!build) {
   180      return React.createElement(EmptyBuild, null)  
   181    }
   182  
   183  
   184    // collapsed active build
   185    if (collapseInfo.collapsed) {
   186      activeTasks = filterActiveTasks(build.tasks, collapseInfo.activeTaskStatuses);
   187      if (activeTasks.length == 0){
   188        return (
   189          React.createElement(CollapsedBuild, {build: build, activeTaskStatuses: collapseInfo.activeTaskStatuses})
   190        )
   191      }
   192      // Can be modified to show combinations of tasks by statuses  
   193      var activeTasks = filterActiveTasks(build.tasks, collapseInfo.activeTaskStatuses)
   194      return (
   195        React.createElement("div", null, 
   196          React.createElement(CollapsedBuild, {build: build, activeTaskStatuses: collapseInfo.activeTaskStatuses}), 
   197          React.createElement(ActiveBuild, {tasks: activeTasks, currentTime: currentTime})
   198        )
   199      )
   200    } 
   201    // uncollapsed active build
   202    return (
   203        React.createElement(ActiveBuild, {tasks: build.tasks, taskFilter: taskFilter, currentTime: currentTime})
   204    )
   205  }
   206  
   207  // At least one task in the version is not inactive, so we display all build tasks with their appropiate colors signifying their status
   208  function ActiveBuild({tasks, taskFilter, currentTime}){  
   209  
   210    if (taskFilter != null){
   211      tasks = _.filter(tasks, function(task){
   212        return task.display_name.toLowerCase().indexOf(taskFilter.toLowerCase()) != -1;
   213      });
   214    }
   215  
   216    return (
   217      React.createElement("div", {className: "active-build"}, 
   218        
   219          _.map(tasks, function(task){
   220            return React.createElement(Task, {task: task, currentTime: currentTime})
   221          })
   222        
   223      )
   224    )
   225  }
   226  
   227  // All tasks are inactive, so we display the words "inactive build"
   228  function InactiveBuild ({}){
   229      return (React.createElement("div", {className: "inactive-build"}, " inactive build "))
   230  }
   231  // No build associated with a given version and variant, so we render an empty div
   232  function EmptyBuild ({}){
   233      return (React.createElement("div", null))
   234  }
   235  
   236  function TooltipContent({task, eta}) {
   237    var topLineContent = task.display_name + " - " + labelFromTask(task);
   238    if (task.status == 'success' || task.status == 'failed') {
   239      var dur = stringifyNanoseconds(task.time_taken);
   240      topLineContent += ' - ' + dur;
   241    }
   242  
   243    if (task.status !='failed' || !task.failed_test_names || task.failed_test_names.length == 0) {
   244      if (task.status == 'started') {
   245        return(
   246          React.createElement("span", {className: "waterfall-tooltip"}, 
   247            topLineContent, " - ", eta
   248          )
   249          )
   250      }
   251      return (
   252          React.createElement("span", {className: "waterfall-tooltip"}, 
   253            topLineContent
   254          )
   255          )
   256    }
   257  
   258    if (task.failed_test_names.length > MaxFailedTestDisplay) {
   259      return (
   260          React.createElement("span", {className: "waterfall-tooltip"}, 
   261            React.createElement("span", null, topLineContent), 
   262          React.createElement("div", {className: "header"}, 
   263            React.createElement("i", {className: "fa fa-times icon"}), 
   264            task.failed_test_names.length, " failed tests" 
   265            )
   266         )
   267          )
   268    }
   269    return(
   270        React.createElement("span", {className: "waterfall-tooltip"}, 
   271          React.createElement("span", null, topLineContent), 
   272        React.createElement("div", {className: "failed-tests"}, 
   273          
   274            task.failed_test_names.map(function(failed_test_name){
   275              return (
   276                  React.createElement("div", null, 
   277                   React.createElement("i", {className: "fa fa-times icon"}), 
   278                    endOfPath(failed_test_name)
   279                  )
   280                  )
   281            })
   282          
   283          )
   284          )
   285        )
   286  }
   287  
   288  // CountdownClock is a class that manages decrementing duration every second.
   289  // It takes as an argument nanosecondsRemaining and begins counting this number
   290  // down as soon as it is instantiated.
   291  class CountdownClock {
   292    constructor(nanosecondsRemaining) {
   293      this.tick = this.tick.bind(this);
   294      this.countdown = setInterval(this.tick, 1000);
   295      this.nanosecondsRemaining = nanosecondsRemaining;
   296    }
   297    tick() {
   298      this.nanosecondsRemaining -= 1 * (1000 * 1000 * 1000);
   299      if (this.nanosecondsRemaining <= 0) {
   300        this.nanosecondsRemaining = 0;
   301        clearInterval(this.countdown);
   302      }
   303    }
   304    getNanosecondsRemaining() {
   305      return this.nanosecondsRemaining;
   306    }
   307  }
   308  
   309  // ETADisplay is a react component that manages displaying a time being
   310  // counted down. It takes as a prop a CountdownClock, which it uses to fetch
   311  // the time left in the count down.
   312  class ETADisplay extends React.Component {
   313    constructor(props) {
   314      super(props);
   315      this.tick = this.tick.bind(this);
   316      this.componentWillUnmount = this.componentWillUnmount.bind(this);
   317  
   318      this.update = setInterval(this.tick, 1000);
   319      this.countdownClock = this.props.countdownClock;
   320  
   321      var nsString = stringifyNanoseconds(this.countdownClock.getNanosecondsRemaining());
   322  
   323      if (this.countdownClock.getNanosecondsRemaining() <= 0) {
   324        nsString = 'unknown';
   325      }
   326      this.state = {
   327        ETAString: nsString
   328      };
   329  
   330    }
   331  
   332    tick() {
   333      var nsRemaining = this.countdownClock.getNanosecondsRemaining();
   334      var nsString = stringifyNanoseconds(nsRemaining);
   335  
   336      if (nsRemaining <= 0) {
   337        nsString = 'unknown';
   338        clearInterval(this.countdown);
   339      }
   340      this.setState({
   341        ETAString: nsString,
   342      });
   343    }
   344  
   345    componentWillUnmount() {
   346      clearInterval(this.interval);
   347    }
   348    render() {
   349      return (React.createElement("span", null, "ETA: ", this.state.ETAString));
   350    }
   351  }
   352  
   353  
   354  // A Task contains the information for a single task for a build, including the link to its page, and a tooltip
   355  function Task({task, currentTime}) {
   356    var OverlayTrigger = ReactBootstrap.OverlayTrigger;
   357    var Popover = ReactBootstrap.Popover;
   358    var Tooltip = ReactBootstrap.Tooltip;
   359    var eta;
   360    if (task.status == 'started') {
   361      var timeRemaining = task.expected_duration - (currentTime - task.start_time);
   362  
   363      var clock = new CountdownClock(timeRemaining);
   364      var eta = (React.createElement(ETADisplay, {countdownClock: clock}));
   365    }
   366    var tooltip = (
   367        React.createElement(Tooltip, {id: "tooltip"}, 
   368          React.createElement(TooltipContent, {task: task, eta: eta})
   369        )
   370        )
   371    return (
   372      React.createElement(OverlayTrigger, {placement: "top", overlay: tooltip, animation: false}, 
   373        React.createElement("a", {href: "/task/" + task.id, className: "waterfall-box " + taskStatusClass(task)})
   374      )
   375    )
   376  }
   377  
   378  // A CollapsedBuild contains a set of PartialProgressBars, which in turn make up a full progress bar
   379  // We iterate over the 5 different main types of task statuses, each of which have a different color association
   380  function CollapsedBuild({build, activeTaskStatuses}){
   381    var taskStats = build.taskStatusCount;
   382  
   383    var taskTypes = {
   384      "success"      : taskStats.succeeded, 
   385      "dispatched"   : taskStats.started, 
   386      "system-failed": taskStats.timed_out,
   387      "undispatched" : taskStats.undispatched, 
   388      "inactive"     : taskStats.inactive,
   389      "failed"       : taskStats.failed,
   390    };
   391  
   392    // Remove all task summaries that have 0 tasks
   393    taskTypes = _.pick(taskTypes, function(count, status){
   394      return count > 0 && !(_.contains(activeTaskStatuses, status))
   395    });
   396    
   397    return (
   398      React.createElement("div", {className: "collapsed-build"}, 
   399        
   400          _.map(taskTypes, function(count, status) {
   401            return React.createElement(TaskSummary, {status: status, count: count, build: build});
   402          }) 
   403        
   404      )
   405    )
   406  }
   407  
   408  // A TaskSummary is the class for one rolled up task type
   409  // A CollapsedBuild is comprised of an  array of contiguous TaskSummaries below individual failing tasks 
   410  function TaskSummary({status, count, build}){
   411    var id_link = "/build/" + build.id;
   412    var OverlayTrigger = ReactBootstrap.OverlayTrigger;
   413    var Popover = ReactBootstrap.Popover;
   414    var Tooltip = ReactBootstrap.Tooltip;
   415    var tt = React.createElement(Tooltip, {id: "tooltip"}, count, " ", status);
   416    var classes = "task-summary " + status
   417    return (
   418      React.createElement(OverlayTrigger, {placement: "top", overlay: tt, animation: false}, 
   419        React.createElement("a", {href: id_link, className: classes}, 
   420          count
   421        )
   422      )
   423    )
   424  }