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 }