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('&#x2023;'));
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