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 }