1/* 2 * Copyright 2010 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17/** 18 * @author: Brett Slatkin (bslatkin@google.com) 19 */ 20 21// Global variables. 22var AUTO_REFRESH = true; 23var ROOT_PIPELINE_ID = null; 24var STATUS_MAP = null; 25var LANG = null; 26 27 28// Adjusts the height/width of the embedded status console iframe. 29function adjustStatusConsole() { 30 var statusConsole = $('#status-console'); 31 var detail = $('#detail'); 32 var sidebar = $('#sidebar'); 33 var control = $('#control'); 34 35 // NOTE: 16 px here is the height of the resize grip in most browsers. 36 // Need to specify this explicitly because some browsers (eg, Firefox) 37 // cause the overflow scrollbars to bounce around the page randomly when 38 // they accidentally overlap the resize grip. 39 if (statusConsole.css('display') == 'none') { 40 var paddingAndMargin = detail.outerHeight() - detail.height() + 16; 41 detail.css('max-height', (sidebar.outerHeight() - paddingAndMargin) + 'px'); 42 } else { 43 detail.css('max-height', '200px'); 44 statusConsole.width( 45 $(window).width() - sidebar.outerWidth()); 46 statusConsole.height( 47 $(window).height() - (statusConsole.offset().top + 16)); 48 } 49} 50 51 52// Gets the ID of the pipeline info in the left-nav. 53function getTreePipelineElementId(value) { 54 if (value.indexOf('#item-pipeline-') == 0) { 55 return value; 56 } else { 57 return '#item-pipeline-' + value; 58 } 59} 60 61 62// Scrolls to element of the pipeline in the tree. 63function scrollTreeToPipeline(pipelineIdOrElement) { 64 var element = pipelineIdOrElement; 65 if (!(pipelineIdOrElement instanceof jQuery)) { 66 element = $(getTreePipelineElementId(pipelineIdOrElement)); 67 } 68 $('#sidebar').scrollTop(element.attr('offsetTop')); 69 $('#sidebar').scrollLeft(element.attr('offsetLeft')); 70} 71 72 73// Opens all pipelines down to the target one if not already expanded and 74// scroll that pipeline into view. 75function expandTreeToPipeline(pipelineId) { 76 if (pipelineId == null) { 77 return; 78 } 79 var elementId = getTreePipelineElementId(pipelineId); 80 var parents = $(elementId).parents('.expandable'); 81 if (parents.size() > 0) { 82 // The toggle function will scroll to highlight the pipeline. 83 parents.children('.hitarea').click(); 84 } else { 85 // No children, so just scroll. 86 scrollTreeToPipeline(pipelineId); 87 } 88} 89 90 91// Handles when the user toggles a leaf of the tree. 92function handleTreeToggle(index, element) { 93 var parentItem = $(element).parent(); 94 var collapsing = parentItem.hasClass('expandable'); 95 if (collapsing) { 96 } else { 97 // When expanded be sure the pipeline and its children are showing. 98 scrollTreeToPipeline(parentItem); 99 } 100} 101 102 103// Counts the number of total and active children for the given pipeline. 104// Will include the supplied pipeline in the totals. 105function countChildren(pipelineId) { 106 var current = STATUS_MAP.pipelines[pipelineId]; 107 if (!current) { 108 return [0, 0]; 109 } 110 var total = 1; 111 var done = 0; 112 if (current.status == 'done') { 113 done += 1; 114 } 115 for (var i = 0, n = current.children.length; i < n; i++) { 116 var parts = countChildren(current.children[i]); 117 total += parts[0]; 118 done += parts[1]; 119 } 120 return [total, done]; 121} 122 123 124// Create the readable name for the pipeline name. 125function prettyName(name, sidebar) { 126 var adjustedName = name; 127 if (sidebar) { 128 var adjustedName = name; 129 var parts = name.split('.'); 130 if (parts.length > 0) { 131 adjustedName = parts[parts.length - 1]; 132 } 133 } 134 return adjustedName.replace(/\./, '.<wbr>'); 135} 136 137 138// Constructs the info div for a stage. 139function constructStageNode(pipelineId, infoMap, sidebar) { 140 if (!infoMap) { 141 return; 142 } 143 var containerDiv = $('<div class="status-box">'); 144 containerDiv.addClass('status-' + infoMap.status); 145 146 var detailDiv = $('<div class="detail-link">'); 147 if (sidebar) { 148 detailDiv.append($('<div class="selected-indicator">').html('‣')); 149 } 150 151 var detailLink = $('<a>'); 152 detailLink.attr('href', '#pipeline-' + pipelineId); 153 detailLink.attr('title', 'ID #' + pipelineId); 154 detailLink.attr('id', 'link-pipeline-' + pipelineId); 155 detailLink.html(prettyName(infoMap.classPath, sidebar)); 156 detailDiv.append(detailLink); 157 containerDiv.append(detailDiv); 158 159 // ID of the pipeline 160 if (!sidebar) { 161 var pipelineIdDiv = $('<div class="status-pipeline-id">'); 162 pipelineIdDiv.text('ID #' + pipelineId); 163 containerDiv.append(pipelineIdDiv); 164 } 165 166 // Broad status category. 167 var statusTitleDiv = $('<div class="status-title">'); 168 if (!sidebar) { 169 statusTitleDiv.append($('<span>').text('Status: ')); 170 } 171 statusTitleDiv.append($('<span>').text(infoMap.status)); 172 containerDiv.append(statusTitleDiv); 173 174 // Determine timing information based on state. 175 var statusTimeLabel = null; 176 var statusTimeMs = null; 177 var statusRuntimeDiv = null; 178 179 if (infoMap.status == 'done') { 180 statusRuntimeDiv = $('<div class="status-runtime">'); 181 182 var statusTimeSpan = $('<span class="status-time-label">'); 183 statusTimeSpan.text('Run time: '); 184 statusRuntimeDiv.append(statusTimeSpan); 185 186 var runtimeSpan = $('<span class="status-runtime-value">'); 187 runtimeSpan.text(getElapsedTimeString( 188 infoMap.startTimeMs, infoMap.endTimeMs)); 189 statusRuntimeDiv.append(runtimeSpan); 190 191 statusTimeLabel = 'Complete'; 192 statusTimeMs = infoMap.endTimeMs; 193 } else if (infoMap.status == 'run') { 194 statusTimeLabel = 'Started'; 195 statusTimeMs = infoMap.startTimeMs; 196 } else if (infoMap.status == 'retry') { 197 statusTimeLabel = 'Will run'; 198 statusTimeMs = infoMap.startTimeMs; 199 } else if (infoMap.status == 'finalizing') { 200 statusTimeLabel = 'Complete'; 201 statusTimeMs = infoMap.endTimeMs; 202 } else if (infoMap.status == 'aborted' || 203 infoMap.status == 'canceled') { 204 statusTimeLabel = 'Aborted'; 205 statusTimeMs = infoMap.endTimeMs; 206 } else if (infoMap.status == 'waiting') { 207 // Do nothing. 208 } 209 210 // Last abort message, if any. 211 if (infoMap.abortMessage) { 212 var abortMessageDiv = $('<div class="status-message abort">'); 213 abortMessageDiv.append($('<span>').text('Abort Message: ')); 214 abortMessageDiv.append( 215 $('<span class="status-message-text">').text(infoMap.abortMessage)); 216 containerDiv.append(abortMessageDiv); 217 } 218 219 // Last error message that caused a retry, if any. 220 if (infoMap.lastRetryMessage) { 221 var errorMessageDiv = $('<div class="status-message error">'); 222 errorMessageDiv.append($('<span>').text('Retry Message: ')); 223 errorMessageDiv.append( 224 $('<span class="status-message-text">').text(infoMap.lastRetryMessage)); 225 containerDiv.append(errorMessageDiv); 226 } 227 228 // User-supplied status message. 229 if (infoMap.statusMessage) { 230 var statusMessageDiv = $('<div class="status-message normal">'); 231 statusMessageDiv.append($('<span>').text('Message: ')); 232 statusMessageDiv.append( 233 $('<span class="status-message-text">').text(infoMap.statusMessage)); 234 containerDiv.append(statusMessageDiv); 235 } 236 237 // Completed children count. 238 if (infoMap.status == 'run' || infoMap.status == 'done') { 239 var counts = countChildren(pipelineId); 240 var totalChildren = counts[0]; 241 var doneChildren = counts[1]; 242 // Do not count ourselves 243 totalChildren--; 244 if (infoMap.status == 'done') { 245 doneChildren--; 246 } 247 if (totalChildren > 0 && doneChildren < totalChildren) { 248 var doneChildrenDiv = $('<div class="active-children">'); 249 doneChildrenDiv.append($('<span>').text('Children: ')); 250 var countText = '' + doneChildren + ' / ' + totalChildren + ' done'; 251 doneChildrenDiv.append($('<span>').text(countText)); 252 containerDiv.append(doneChildrenDiv); 253 } 254 } 255 256 // Number of attempts, if more than one. 257 if (infoMap.currentAttempt > 1) { 258 var attemptDiv = $('<div class="status-attempt">'); 259 var attemptTitle = 'Attempt: '; 260 if (infoMap.status == 'retry') { 261 attemptTitle = 'Next Attempt: '; 262 } else if (infoMap.status == 'done') { 263 attemptTitle = 'Attempts: '; 264 } 265 attemptDiv.append($('<span>').text(attemptTitle)); 266 var attemptText = '' + infoMap.currentAttempt + ' / ' + 267 infoMap.maxAttempts + ''; 268 attemptDiv.append($('<span>').text(attemptText)); 269 containerDiv.append(attemptDiv); 270 } 271 272 // Runtime if present. 273 if (statusRuntimeDiv) { 274 containerDiv.append(statusRuntimeDiv); 275 } 276 277 // Next retry time, complete time, start time. 278 if (statusTimeLabel && statusTimeMs) { 279 var statusTimeDiv = $('<div class="status-time">'); 280 281 var statusTimeSpan = $('<span class="status-time-label">'); 282 statusTimeSpan.text(statusTimeLabel + ': '); 283 statusTimeDiv.append(statusTimeSpan); 284 285 var sinceSpan = $('<abbr class="timeago status-time-since">'); 286 var isoDate = getIso8601String(statusTimeMs); 287 sinceSpan.attr('title', isoDate); 288 sinceSpan.text(isoDate); 289 sinceSpan.timeago(); 290 statusTimeDiv.append(sinceSpan); 291 292 containerDiv.append(statusTimeDiv); 293 } 294 295 // User-supplied status links. 296 var linksDiv = $('<div class="status-links">'); 297 if (!sidebar) { 298 linksDiv.append($('<span>').text('Links: ')); 299 } 300 var foundLinks = 0; 301 if (infoMap.statusConsoleUrl) { 302 var link = $('<a class="status-console">'); 303 link.attr('href', infoMap.statusConsoleUrl); 304 link.text('Console'); 305 link.click(function(event) { 306 selectPipeline(pipelineId); 307 event.preventDefault(); 308 }); 309 linksDiv.append(link); 310 foundLinks++; 311 } 312 if (infoMap.statusLinks) { 313 $.each(infoMap.statusLinks, function(key, value) { 314 var link = $('<a>'); 315 link.attr('href', value); 316 link.text(key); 317 link.click(function(event) { 318 selectPipeline(pipelineId, key); 319 event.preventDefault(); 320 }); 321 linksDiv.append(link); 322 foundLinks++; 323 }); 324 } 325 if (foundLinks > 0) { 326 containerDiv.append(linksDiv); 327 } 328 329 // Retry parameters. 330 if (!sidebar) { 331 var retryParamsDiv = $('<div class="status-retry-params">'); 332 retryParamsDiv.append( 333 $('<div class="retry-params-title">').text('Retry parameters')); 334 335 var backoffSecondsDiv = $('<div class="retry-param">'); 336 $('<span>').text('Backoff seconds: ').appendTo(backoffSecondsDiv); 337 $('<span>') 338 .text(infoMap.backoffSeconds) 339 .appendTo(backoffSecondsDiv); 340 retryParamsDiv.append(backoffSecondsDiv); 341 342 var backoffFactorDiv = $('<div class="retry-param">'); 343 $('<span>').text('Backoff factor: ').appendTo(backoffFactorDiv); 344 $('<span>') 345 .text(infoMap.backoffFactor) 346 .appendTo(backoffFactorDiv); 347 retryParamsDiv.append(backoffFactorDiv); 348 349 containerDiv.append(retryParamsDiv); 350 } 351 352 function renderCollapsableValue(value, container) { 353 var stringValue = $.toJSON(value); 354 var SPLIT_LENGTH = 200; 355 if (stringValue.length < SPLIT_LENGTH) { 356 container.append($('<span>').text(stringValue)); 357 return; 358 } 359 360 var startValue = stringValue.substr(0, SPLIT_LENGTH); 361 var endValue = stringValue.substr(SPLIT_LENGTH); 362 363 // Split the end value with <wbr> tags so it looks nice; force 364 // word wrapping never works right. 365 var moreSpan = $('<span class="value-disclosure-more">'); 366 for (var i = 0; i < endValue.length; i += SPLIT_LENGTH) { 367 moreSpan.append(endValue.substr(i, SPLIT_LENGTH)); 368 moreSpan.append('<wbr/>'); 369 } 370 var betweenMoreText = '...(' + endValue.length + ' more) '; 371 var betweenSpan = $('<span class="value-disclosure-between">') 372 .text(betweenMoreText); 373 var toggle = $('<a class="value-disclosure-toggle">') 374 .text('Expand') 375 .attr('href', ''); 376 toggle.click(function(e) { 377 e.preventDefault(); 378 if (moreSpan.css('display') == 'none') { 379 betweenSpan.text(' '); 380 toggle.text('Collapse'); 381 } else { 382 betweenSpan.text(betweenMoreText); 383 toggle.text('Expand'); 384 } 385 moreSpan.toggle(); 386 }); 387 container.append($('<span>').text(startValue)); 388 container.append(moreSpan); 389 container.append(betweenSpan); 390 container.append(toggle); 391 } 392 393 // Slot rendering 394 function renderSlot(slotKey) { 395 var filledMessage = null; 396 var slot = STATUS_MAP.slots[slotKey]; 397 var slotDetailDiv = $('<div class="slot-detail">'); 398 if (!slot) { 399 var keyAbbr = $('<abbr>'); 400 keyAbbr.attr('title', slotKey); 401 keyAbbr.text('Pending slot'); 402 slotDetailDiv.append(keyAbbr); 403 return slotDetailDiv; 404 } 405 406 if (slot.status == 'filled') { 407 var valueDiv = $('<span class="slot-value-container">'); 408 valueDiv.append($('<span>').text('Value: ')); 409 var valueContainer = $('<span class="slot-value">'); 410 renderCollapsableValue(slot.value, valueContainer); 411 valueDiv.append(valueContainer); 412 slotDetailDiv.append(valueDiv); 413 414 var filledDiv = $('<div class="slot-filled">'); 415 filledDiv.append($('<span>').text('Filled: ')); 416 var isoDate = getIso8601String(slot.fillTimeMs); 417 filledDiv.append( 418 $('<abbr class="timeago">') 419 .attr('title', isoDate) 420 .text(isoDate) 421 .timeago()); 422 slotDetailDiv.append(filledDiv); 423 424 filledMessage = 'Filled by'; 425 } else { 426 filledMessage = 'Waiting for'; 427 } 428 429 var filledMessageDiv = $('<div class="slot-message">'); 430 filledMessageDiv.append( 431 $('<span>').text(filledMessage + ': ')); 432 var otherPipeline = STATUS_MAP.pipelines[slot.fillerPipelineId]; 433 if (otherPipeline) { 434 var fillerLink = $('<a class="slot-filler">'); 435 fillerLink 436 .attr('title', 'ID #' + slot.fillerPipelineId) 437 .attr('href', '#pipeline-' + slot.fillerPipelineId) 438 .text(otherPipeline.classPath); 439 fillerLink.click(function(event) { 440 selectPipeline(slot.fillerPipelineId); 441 event.preventDefault(); 442 }); 443 filledMessageDiv.append(fillerLink); 444 } else { 445 filledMessageDiv.append( 446 $('<span class="status-pipeline-id">') 447 .text('ID #' + slot.fillerPipelineId)); 448 } 449 slotDetailDiv.append(filledMessageDiv); 450 return slotDetailDiv; 451 } 452 453 // Argument/ouptut rendering 454 function renderParam(key, valueDict) { 455 var paramDiv = $('<div class="status-param">'); 456 457 var nameDiv = $('<span class="status-param-name">'); 458 nameDiv.text(key + ':'); 459 paramDiv.append(nameDiv); 460 461 if (valueDict.type == 'slot' && STATUS_MAP.slots) { 462 paramDiv.append(renderSlot(valueDict.slotKey)); 463 } else { 464 var valueDiv = $('<span class="status-param-value">'); 465 renderCollapsableValue(valueDict.value, valueDiv); 466 paramDiv.append(valueDiv); 467 } 468 469 return paramDiv; 470 } 471 472 if (!sidebar && ( 473 !$.isEmptyObject(infoMap.kwargs) || infoMap.args.length > 0)) { 474 var paramDiv = $('<div class="param-container">'); 475 paramDiv.append( 476 $('<div class="param-container-title">') 477 .text('Parameters')); 478 479 // Positional arguments 480 $.each(infoMap.args, function(index, valueDict) { 481 paramDiv.append(renderParam(index, valueDict)); 482 }); 483 484 // Keyword arguments in alphabetical order 485 var keywordNames = []; 486 $.each(infoMap.kwargs, function(key, value) { 487 keywordNames.push(key); 488 }); 489 keywordNames.sort(); 490 $.each(keywordNames, function(index, key) { 491 paramDiv.append(renderParam(key, infoMap.kwargs[key])); 492 }); 493 494 containerDiv.append(paramDiv); 495 } 496 497 // Outputs in alphabetical order, but default first 498 if (!sidebar) { 499 var outputContinerDiv = $('<div class="outputs-container">'); 500 outputContinerDiv.append( 501 $('<div class="outputs-container-title">') 502 .text('Outputs')); 503 504 var outputNames = []; 505 $.each(infoMap.outputs, function(key, value) { 506 if (key != 'default') { 507 outputNames.push(key); 508 } 509 }); 510 outputNames.sort(); 511 outputNames.unshift('default'); 512 513 $.each(outputNames, function(index, key) { 514 outputContinerDiv.append(renderParam( 515 key, {'type': 'slot', 'slotKey': infoMap.outputs[key]})); 516 }); 517 518 containerDiv.append(outputContinerDiv); 519 } 520 521 // Related pipelines 522 function renderRelated(relatedList, relatedTitle, classPrefix) { 523 var relatedDiv = $('<div>'); 524 relatedDiv.addClass(classPrefix + '-container'); 525 relatedTitleDiv = $('<div>'); 526 relatedTitleDiv.addClass(classPrefix + '-container-title'); 527 relatedTitleDiv.text(relatedTitle); 528 relatedDiv.append(relatedTitleDiv); 529 530 $.each(relatedList, function(index, relatedPipelineId) { 531 var relatedInfoMap = STATUS_MAP.pipelines[relatedPipelineId]; 532 if (relatedInfoMap) { 533 var relatedLink = $('<a>'); 534 relatedLink 535 .addClass(classPrefix + '-link') 536 .attr('title', 'ID #' + relatedPipelineId) 537 .attr('href', '#pipeline-' + relatedPipelineId) 538 .text(relatedInfoMap.classPath); 539 relatedLink.click(function(event) { 540 selectPipeline(relatedPipelineId); 541 event.preventDefault(); 542 }); 543 relatedDiv.append(relatedLink); 544 } else { 545 var relatedIdDiv = $('<div>'); 546 relatedIdDiv 547 .addClass(classPrefix + '-pipeline-id') 548 .text('ID #' + relatedPipelineId); 549 relatedDiv.append(relatedIdDiv); 550 } 551 }); 552 553 return relatedDiv; 554 } 555 556 // Run after 557 if (!sidebar && infoMap.afterSlotKeys.length > 0) { 558 var foundPipelineIds = []; 559 $.each(infoMap.afterSlotKeys, function(index, slotKey) { 560 if (STATUS_MAP.slots[slotKey]) { 561 var slotDict = STATUS_MAP.slots[slotKey]; 562 if (slotDict.fillerPipelineId) { 563 foundPipelineIds.push(slotDict.fillerPipelineId); 564 } 565 } 566 }); 567 containerDiv.append( 568 renderRelated(foundPipelineIds, 'Run after', 'run-after')); 569 } 570 571 // Spawned children 572 if (!sidebar && infoMap.children.length > 0) { 573 containerDiv.append( 574 renderRelated(infoMap.children, 'Children', 'child')); 575 } 576 577 return containerDiv; 578} 579 580 581// Recursively creates the sidebar. Use null nextPipelineId to create from root. 582function generateSidebar(statusMap, nextPipelineId, rootElement) { 583 var currentElement = null; 584 585 if (nextPipelineId) { 586 currentElement = $('<li>'); 587 // Value should match return of getTreePipelineElementId 588 currentElement.attr('id', 'item-pipeline-' + nextPipelineId); 589 } else { 590 currentElement = rootElement; 591 nextPipelineId = statusMap.rootPipelineId; 592 } 593 594 var parentInfoMap = statusMap.pipelines[nextPipelineId]; 595 currentElement.append( 596 constructStageNode(nextPipelineId, parentInfoMap, true)); 597 598 if (statusMap.pipelines[nextPipelineId]) { 599 var children = statusMap.pipelines[nextPipelineId].children; 600 if (children.length > 0) { 601 var treeElement = null; 602 if (rootElement) { 603 treeElement = 604 $('<ul id="pipeline-tree" class="treeview-black treeview">'); 605 } else { 606 treeElement = $('<ul>'); 607 } 608 609 $.each(children, function(index, childPipelineId) { 610 var childElement = generateSidebar(statusMap, childPipelineId); 611 treeElement.append(childElement); 612 }); 613 currentElement.append(treeElement); 614 } 615 } 616 return currentElement; 617} 618 619 620function selectPipeline(pipelineId, linkName) { 621 if (linkName) { 622 location.hash = '#pipeline-' + pipelineId + ';' + linkName; 623 } else { 624 location.hash = '#pipeline-' + pipelineId; 625 } 626} 627 628 629// Depth-first search for active pipeline. 630function findActivePipeline(pipelineId, isRoot) { 631 var infoMap = STATUS_MAP.pipelines[pipelineId]; 632 if (!infoMap) { 633 return null; 634 } 635 636 // This is an active leaf node. 637 if (infoMap.children.length == 0 && infoMap.status != 'done') { 638 return pipelineId; 639 } 640 641 // Sort children by start time only. 642 var children = infoMap.children.slice(0); 643 children.sort(function(a, b) { 644 var infoMapA = STATUS_MAP.pipelines[a]; 645 var infoMapB = STATUS_MAP.pipelines[b]; 646 if (!infoMapA || !infoMapB) { 647 return 0; 648 } 649 if (infoMapA.startTimeMs && infoMapB.startTimeMs) { 650 return infoMapA.startTimeMs - infoMapB.startTimeMs; 651 } else { 652 return 0; 653 } 654 }); 655 656 for (var i = 0; i < children.length; ++i) { 657 var foundPipelineId = findActivePipeline(children[i], false); 658 if (foundPipelineId != null) { 659 return foundPipelineId; 660 } 661 } 662 663 return null; 664} 665 666 667function getSelectedPipelineId() { 668 var prefix = '#pipeline-'; 669 var pieces = location.hash.split(';', 2); 670 if (pieces[0].indexOf(prefix) == 0) { 671 return pieces[0].substr(prefix.length); 672 } 673 return null; 674} 675 676 677/* Event handlers */ 678function handleHashChange() { 679 var prefix = '#pipeline-'; 680 var hash = location.hash; 681 var pieces = hash.split(';', 2); 682 var pipelineId = null; 683 684 if (pieces[0].indexOf(prefix) == 0) { 685 pipelineId = pieces[0].substr(prefix.length); 686 } else { 687 // Bad hash, just show the root pipeline. 688 location.hash = ''; 689 return; 690 } 691 692 if (!pipelineId) { 693 // No hash means show the root pipeline. 694 pipelineId = STATUS_MAP.rootPipelineId; 695 } 696 var rootMap = STATUS_MAP.pipelines[STATUS_MAP.rootPipelineId]; 697 var infoMap = STATUS_MAP.pipelines[pipelineId]; 698 if (!rootMap || !infoMap) { 699 // Hash not found. 700 return; 701 } 702 703 // Clear any selection styling. 704 $('.selected-link').removeClass('selected-link'); 705 706 if (pieces[1]) { 707 // Show a specific status link. 708 var statusLink = $(getTreePipelineElementId(pipelineId)) 709 .find('.status-links>a:contains("' + pieces[1] + '")'); 710 if (statusLink.size() > 0) { 711 var selectedLink = $(statusLink[0]); 712 selectedLink.addClass('selected-link'); 713 $('#status-console').attr('src', selectedLink.attr('href')); 714 $('#status-console').show(); 715 } else { 716 // No console link for this pipeline; ignore it. 717 $('#status-console').hide(); 718 } 719 } else { 720 // Show the console link. 721 var consoleLink = $(getTreePipelineElementId(pipelineId)) 722 .find('a.status-console'); 723 if (consoleLink.size() > 0) { 724 var selectedLink = $(consoleLink[0]); 725 selectedLink.addClass('selected-link'); 726 $('#status-console').attr('src', selectedLink.attr('href')); 727 $('#status-console').show(); 728 } else { 729 // No console link for this pipeline; ignore it. 730 $('#status-console').hide(); 731 } 732 } 733 734 // Mark the pipeline as selected. 735 var selected = $('#link-pipeline-' + pipelineId); 736 selected.addClass('selected-link'); 737 selected.parents('.status-box').addClass('selected-link'); 738 739 // Title is always the info for the root pipeline, to make it easier to 740 // track across multiple tabs. 741 document.title = rootMap.classPath + ' - ID #' + STATUS_MAP.rootPipelineId; 742 743 // Update the detail status frame. 744 var stageNode = constructStageNode(pipelineId, infoMap, false); 745 $('#overview').remove(); 746 stageNode.attr('id', 'overview'); 747 $('#detail').append(stageNode); 748 749 // Make sure everything is the right size. 750 adjustStatusConsole(); 751} 752 753 754function handleAutoRefreshClick(event) { 755 var loc = window.location; 756 var newSearch = null; 757 if (!AUTO_REFRESH && event.target.checked) { 758 newSearch = '?root=' + ROOT_PIPELINE_ID; 759 } else if (AUTO_REFRESH && !event.target.checked) { 760 newSearch = '?root=' + ROOT_PIPELINE_ID + '&auto=false'; 761 } 762 763 if (newSearch != null) { 764 loc.replace( 765 loc.protocol + '//' + loc.host + loc.pathname + 766 newSearch + loc.hash); 767 } 768} 769 770 771function handleRefreshClick(event) { 772 var loc = window.location; 773 if (AUTO_REFRESH) { 774 newSearch = '?root=' + ROOT_PIPELINE_ID; 775 } else { 776 newSearch = '?root=' + ROOT_PIPELINE_ID + '&auto=false'; 777 } 778 loc.href = loc.protocol + '//' + loc.host + loc.pathname + newSearch; 779 return false; 780} 781 782function handleDeleteClick(event) { 783 var ajaxRequest = { 784 type: 'GET', 785 url: 'rpc/delete?root_pipeline_id=' + ROOT_PIPELINE_ID, 786 dataType: 'text', 787 error: function(request, textStatus) { 788 if (request.status == 404) { 789 setButter('Pipeline is already deleted'); 790 } else { 791 setButter('Delete request failed: ' + textStatus); 792 } 793 window.setTimeout(function() { 794 clearButter(); 795 }, 5000); 796 }, 797 success: function(data, textStatus, request) { 798 setButter('Delete request was sent'); 799 window.setTimeout(function() { 800 window.location.href = 'list'; 801 }, 5000); 802 } 803 }; 804 $.ajax(jQuery.extend({}, ajaxRequest)); 805} 806 807function handleAbortClick(event) { 808 var ajaxRequest = { 809 type: 'GET', 810 url: 'rpc/abort?root_pipeline_id=' + ROOT_PIPELINE_ID, 811 dataType: 'text', 812 error: function(request, textStatus) { 813 setButter('Abort request failed: ' + textStatus); 814 window.setTimeout(function() { 815 clearButter(); 816 }, 5000); 817 }, 818 success: function(data, textStatus, request) { 819 setButter('Abort request was sent'); 820 window.setTimeout(function() { 821 clearButter(); 822 window.location.reload(); 823 }, 5000); 824 } 825 }; 826 if (confirm('Are you sure you want to abort the pipeline', 'Abort')) { 827 $.ajax(jQuery.extend({}, ajaxRequest)); 828 } 829} 830 831/* Initialization. */ 832function initStatus() { 833 if (window.location.search.length > 0 && 834 window.location.search[0] == '?') { 835 var query = window.location.search.substr(1); 836 var pieces = query.split('&'); 837 $.each(pieces, function(index, param) { 838 var mapping = param.split('='); 839 if (mapping.length != 2) { 840 return; 841 } 842 if (mapping[0] == 'auto' && mapping[1] == 'false') { 843 AUTO_REFRESH = false; 844 } else if (mapping[0] == 'root') { 845 ROOT_PIPELINE_ID = mapping[1]; 846 if (ROOT_PIPELINE_ID.match(/^pipeline-/)) { 847 ROOT_PIPELINE_ID = ROOT_PIPELINE_ID.substring(9); 848 } 849 } 850 }); 851 } 852 853 if (!Boolean(ROOT_PIPELINE_ID)) { 854 setButter('Missing root param' + 855 '. For a job list click <a href="list">here</a>.', 856 true, null, true); 857 return; 858 } 859 860 var loadingMsg = 'Loading... #' + ROOT_PIPELINE_ID; 861 var attempts = 1; 862 var ajaxRequest = { 863 type: 'GET', 864 url: 'rpc/tree?root_pipeline_id=' + ROOT_PIPELINE_ID, 865 dataType: 'text', 866 error: function(request, textStatus) { 867 if (request.status == 404) { 868 if (++attempts <= 5) { 869 setButter(loadingMsg + ' [attempt #' + attempts + ']'); 870 window.setTimeout(function() { 871 $.ajax(jQuery.extend({}, ajaxRequest)); 872 }, 2000); 873 } else { 874 setButter('Could not find pipeline #' + ROOT_PIPELINE_ID + 875 '. For a job list click <a href="list">here</a>.', 876 true, null, true); 877 } 878 } else if (request.status == 449) { 879 var root = request.getResponseHeader('root_pipeline_id'); 880 var newURL = '?root=' + root + '#pipeline-' + ROOT_PIPELINE_ID; 881 window.location.replace(newURL); 882 } else { 883 getResponseDataJson(textStatus); 884 } 885 }, 886 success: function(data, textStatus, request) { 887 var response = getResponseDataJson(null, data); 888 if (response) { 889 clearButter(); 890 STATUS_MAP = response; 891 LANG = request.getResponseHeader('Pipeline-Lang'); 892 initStatusDone(); 893 } 894 } 895 }; 896 setButter(loadingMsg); 897 $.ajax(jQuery.extend({}, ajaxRequest)); 898} 899 900 901function initStatusDone() { 902 jQuery.timeago.settings.allowFuture = true; 903 904 // Update the root pipeline ID to match what the server returns. This handles 905 // the case where the ID specified is for a child node. We always want to 906 // show status up to the root. 907 ROOT_PIPELINE_ID = STATUS_MAP.rootPipelineId; 908 909 // Generate the sidebar. 910 generateSidebar(STATUS_MAP, null, $('#sidebar')); 911 912 // Turn the sidebar into a tree. 913 $('#pipeline-tree').treeview({ 914 collapsed: true, 915 unique: false, 916 cookieId: 'pipeline Id here', 917 toggle: handleTreeToggle 918 }); 919 $('#sidebar').show(); 920 921 var rootStatus = STATUS_MAP.pipelines[STATUS_MAP.rootPipelineId].status; 922 var isFinalState = /^done$|^aborted$|^canceled$/.test(rootStatus); 923 924 // Init the control panel. 925 $('#auto-refresh').click(handleAutoRefreshClick); 926 if (!AUTO_REFRESH) { 927 $('#auto-refresh').attr('checked', ''); 928 } else { 929 if (!isFinalState) { 930 // Only do auto-refresh behavior if we're not in a terminal state. 931 window.setTimeout(function() { 932 var loc = window.location; 933 var search = '?root=' + ROOT_PIPELINE_ID; 934 loc.replace(loc.protocol + '//' + loc.host + loc.pathname + search); 935 }, 30 * 1000); 936 } 937 } 938 $('.refresh-link').click(handleRefreshClick); 939 $('.abort-link').click(handleAbortClick); 940 $('.delete-link').click(handleDeleteClick); 941 if (LANG == 'Java') { 942 if (isFinalState) { 943 $('.delete-link').show(); 944 } else { 945 $('.abort-link').show(); 946 } 947 } 948 $('#control').show(); 949 950 // Properly adjust the console iframe to match the window size. 951 $(window).resize(adjustStatusConsole); 952 window.setTimeout(adjustStatusConsole, 0); 953 954 // Handle ajax-y URL fragment events. 955 $(window).hashchange(handleHashChange); 956 $(window).hashchange(); // Trigger for initial load. 957 958 // When there's no hash selected, auto-navigate to the most active node. 959 if (window.location.hash == '' || window.location.hash == '#') { 960 var activePipelineId = findActivePipeline(STATUS_MAP.rootPipelineId, true); 961 if (activePipelineId) { 962 selectPipeline(activePipelineId); 963 } else { 964 // If there's nothing active, then select the root. 965 selectPipeline(ROOT_PIPELINE_ID); 966 } 967 } 968 969 // Scroll to the current active node. 970 expandTreeToPipeline(getSelectedPipelineId()); 971} 972