github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/public/static/js/jsx/tasks_grid.jsx (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      <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 <Variant row={row} project={project} collapseInfo={collapseInfo} versions={data.versions} taskFilter={taskFilter} currentTime={data.current_time}/>;
   127          })
   128        }
   129      </div> 
   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        <div className="row variant-row">
   144          <div className="col-xs-2 build-variants"> 
   145            {row.build_variant.display_name}
   146          </div>
   147          <div className="col-xs-10"> 
   148            <div className="row build-cells">
   149              {
   150                versions.map(function(version, i){
   151                    return(<div className="waterfall-build">
   152                      <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                    </div>
   159                    );
   160                })
   161              }
   162            </div>
   163          </div>
   164        </div>
   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 <InactiveBuild/>;
   176    }
   177  
   178    // no build for this version
   179    if (!build) {
   180      return <EmptyBuild />  
   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          <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        <div>
   196          <CollapsedBuild build={build} activeTaskStatuses={collapseInfo.activeTaskStatuses} />
   197          <ActiveBuild tasks={activeTasks} currentTime={currentTime}/>
   198        </div>
   199      )
   200    } 
   201    // uncollapsed active build
   202    return (
   203        <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      <div className="active-build"> 
   218        {
   219          _.map(tasks, function(task){
   220            return <Task task={task} currentTime={currentTime}/>
   221          })
   222        }
   223      </div>
   224    )
   225  }
   226  
   227  // All tasks are inactive, so we display the words "inactive build"
   228  function InactiveBuild ({}){
   229      return (<div className="inactive-build"> inactive build </div>)
   230  }
   231  // No build associated with a given version and variant, so we render an empty div
   232  function EmptyBuild ({}){
   233      return (<div></div>)
   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          <span className="waterfall-tooltip">
   247            {topLineContent} - {eta}
   248          </span>
   249          )
   250      }
   251      return (
   252          <span className="waterfall-tooltip">
   253            {topLineContent}
   254          </span>
   255          )
   256    }
   257  
   258    if (task.failed_test_names.length > MaxFailedTestDisplay) {
   259      return (
   260          <span className="waterfall-tooltip">
   261            <span>{topLineContent}</span> 
   262          <div className="header">
   263            <i className="fa fa-times icon"></i>
   264            {task.failed_test_names.length} failed tests 
   265            </div>
   266         </span>
   267          )
   268    }
   269    return(
   270        <span className="waterfall-tooltip">
   271          <span>{topLineContent}</span>
   272        <div className="failed-tests">
   273          {
   274            task.failed_test_names.map(function(failed_test_name){
   275              return (
   276                  <div> 
   277                   <i className="fa fa-times icon"></i>
   278                    {endOfPath(failed_test_name)} 
   279                  </div>
   280                  )
   281            })
   282          }
   283          </div>
   284          </span>
   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 (<span>ETA: {this.state.ETAString}</span>);
   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 = (<ETADisplay countdownClock={clock} />);
   365    }
   366    var tooltip = (
   367        <Tooltip id="tooltip">
   368          <TooltipContent task={task}  eta={eta}/>
   369        </Tooltip>
   370        )
   371    return (
   372      <OverlayTrigger placement="top" overlay={tooltip} animation={false}>
   373        <a href={"/task/" + task.id} className={"waterfall-box " + taskStatusClass(task)} />  
   374      </OverlayTrigger>
   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      <div className="collapsed-build">
   399        {
   400          _.map(taskTypes, function(count, status) {
   401            return <TaskSummary status={status} count={count} build={build} />;
   402          }) 
   403        }
   404      </div>
   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 = <Tooltip id="tooltip">{count} {status}</Tooltip>;
   416    var classes = "task-summary " + status
   417    return (
   418      <OverlayTrigger placement="top" overlay={tt} animation={false}>
   419        <a href={id_link} className={classes}>
   420          {count}
   421        </a>
   422      </OverlayTrigger>
   423    )
   424  }