Home | History | Annotate | Download | only in scripts
      1 /*
      2  * Copyright (C) 2011 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions
      6  * are met:
      7  * 1. Redistributions of source code must retain the above copyright
      8  *    notice, this list of conditions and the following disclaimer.
      9  * 2. Redistributions in binary form must reproduce the above copyright
     10  *    notice, this list of conditions and the following disclaimer in the
     11  *    documentation and/or other materials provided with the distribution.
     12  *
     13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
     14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
     15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
     17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
     18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
     19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
     20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
     21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
     22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
     23  * THE POSSIBILITY OF SUCH DAMAGE.
     24  */
     25 
     26 var ui = ui || {};
     27 
     28 (function () {
     29 
     30 ui.displayURLForBuilder = function(builderName)
     31 {
     32     return config.waterfallURL + '?' + $.param({
     33         'builder': builderName
     34     });
     35 }
     36 
     37 ui.kUseNewWindowForLinksSetting = 'gardenomatic.use-new-window-for-links';
     38 
     39 ui.displayNameForBuilder = function(builderName)
     40 {
     41     return builderName.replace(/Webkit /, '');
     42 }
     43 
     44 ui.urlForTest = function(testName)
     45 {
     46     return 'http://trac.webkit.org/browser/trunk/LayoutTests/' + testName;
     47 }
     48 
     49 ui.urlForCrbug = function(bugID)
     50 {
     51     return 'http://crbug.com/' + bugID;
     52 }
     53 
     54 ui.urlForFlakinessDashboard = function(opt_testNameList)
     55 {
     56     var testsParameter = opt_testNameList ? opt_testNameList.join(',') : '';
     57     return 'http://test-results.appspot.com/dashboards/flakiness_dashboard.html#tests=' + encodeURIComponent(testsParameter);
     58 }
     59 
     60 ui.urlForEmbeddedFlakinessDashboard = function(opt_testNameList)
     61 {
     62     return ui.urlForFlakinessDashboard(opt_testNameList) + '&showChrome=false';
     63 }
     64 
     65 ui.rolloutReasonForTestNameList = function(testNameList)
     66 {
     67     return 'Broke:\n' + testNameList.map(function(testName) {
     68         return '* ' + testName;
     69     }).join('\n');
     70 }
     71 
     72 ui.setTargetForLink = function(anchor)
     73 {
     74     if (anchor.href.indexOf('#') === 0)
     75         return;
     76     if (ui.useNewWindowForLinks)
     77         anchor.target = '_blank';
     78     else
     79         anchor.removeAttribute('target');
     80 }
     81 
     82 ui.setUseNewWindowForLinks = function(enabled)
     83 {
     84     ui.useNewWindowForLinks = enabled;
     85     if (enabled)
     86         localStorage[ui.kUseNewWindowForLinksSetting] = 'true';
     87     else
     88         delete localStorage[ui.kUseNewWindowForLinksSetting];
     89 
     90     $('a').each(function() {
     91         ui.setTargetForLink(this);
     92     });
     93 }
     94 ui.setUseNewWindowForLinks(!!localStorage[ui.kUseNewWindowForLinksSetting]);
     95 
     96 ui.createLinkNode = function(url, textContent)
     97 {
     98     var link = document.createElement('a');
     99     link.href = url;
    100     ui.setTargetForLink(link);
    101     link.appendChild(document.createTextNode(textContent));
    102     return link;
    103 }
    104 
    105 ui.onebar = base.extends('div', {
    106     init: function()
    107     {
    108         this.id = 'onebar';
    109         this.innerHTML =
    110             '<ul>' +
    111                 '<li><a href="#unexpected">Unexpected Failures</a></li>' +
    112                 '<li><a href="#expected">Expected Failures</a></li>' +
    113                 '<li><a href="#results">Results</a></li>' +
    114             '</ul>' +
    115             '<div id="link-handling"><input type="checkbox" id="new-window-for-links"><label for="new-window-for-links">Open links in new window</label></div>' +
    116             '<div id="unexpected"></div>' +
    117             '<div id="expected"></div>' +
    118             '<div id="results"></div>';
    119         this._tabNames = [
    120             'unexpected',
    121             'expected',
    122             'results',
    123         ]
    124 
    125         this._tabIndexToSavedScrollOffset = {};
    126         this._tabs = $(this).tabs({
    127             disabled: [2],
    128             show: function(event, ui) { this._restoreScrollOffset(ui.index); },
    129             select: function(event, ui) {
    130                 this._saveScrollOffset();
    131                 window.location.hash = ui.tab.hash;
    132             }.bind(this)
    133         });
    134     },
    135     _saveScrollOffset: function() {
    136         var tabIndex = this._tabs.tabs('option', 'selected');
    137         this._tabIndexToSavedScrollOffset[tabIndex] = document.body.scrollTop;
    138     },
    139     _restoreScrollOffset: function(tabIndex)
    140     {
    141         document.body.scrollTop = this._tabIndexToSavedScrollOffset[tabIndex] || 0;
    142     },
    143     _setupHistoryHandlers: function()
    144     {
    145         function currentHash() {
    146             var hash = window.location.hash;
    147             return (!hash || hash == '#') ? '#unexpected' : hash;
    148         }
    149 
    150         var self = this;
    151         window.onhashchange = function(event) {
    152             var tabName = currentHash().substring(1);
    153             self._selectInternal(tabName);
    154         };
    155 
    156         // When navigating from the browser chrome, we'll
    157         // scroll to the #tabname contents. popstate fires before
    158         // we scroll, so we can save the scroll offset first.
    159         window.onpopstate = function() {
    160             self._saveScrollOffset();
    161         };
    162     },
    163     _setupLinkSettingHandler: function()
    164     {
    165         $('#new-window-for-links').attr('checked', ui.useNewWindowForLinks);
    166         $('#new-window-for-links').change(function(event) {
    167             ui.setUseNewWindowForLinks(this.checked);
    168         });
    169     },
    170     attach: function()
    171     {
    172         document.body.insertBefore(this, document.body.firstChild);
    173         this._setupLinkSettingHandler();
    174         this._setupHistoryHandlers();
    175     },
    176     tabNamed: function(tabName)
    177     {
    178         if (this._tabNames.indexOf(tabName) == -1)
    179             return null;
    180         tab = document.getElementById(tabName);
    181         // We perform this sanity check below to make sure getElementById
    182         // hasn't given us a node in some other unrelated part of the document.
    183         // that shouldn't happen normally, but it could happen if an attacker
    184         // has somehow sneakily added a node to our document.
    185         if (tab.parentNode != this)
    186             return null;
    187         return tab;
    188     },
    189     unexpected: function()
    190     {
    191         return this.tabNamed('unexpected');
    192     },
    193     expected: function()
    194     {
    195         return this.tabNamed('expected');
    196     },
    197     results: function()
    198     {
    199         return this.tabNamed('results');
    200     },
    201     _selectInternal: function(tabName) {
    202         var tabIndex = this._tabNames.indexOf(tabName);
    203         this._tabs.tabs('enable', tabIndex);
    204         this._tabs.tabs('select', tabIndex);
    205     },
    206     select: function(tabName)
    207     {
    208         this._saveScrollOffset();
    209         this._selectInternal(tabName);
    210         window.location = '#' + tabName;
    211     }
    212 });
    213 
    214 ui.TreeStatus = base.extends('div',  {
    215     addStatus: function(name)
    216     {
    217         var label = document.createElement('div');
    218         label.textContent = " " + name + ' status: ';
    219         this.appendChild(label);
    220         var statusSpan = document.createElement('span');
    221         statusSpan.textContent = '(Loading...) ';
    222         label.appendChild(statusSpan);
    223         treestatus.fetchTreeStatus(treestatus.urlByName(name), statusSpan);
    224     },
    225     init: function()
    226     {
    227         this.className = 'treestatus';
    228         this.addStatus('blink');
    229         this.addStatus('chromium');
    230     },
    231 });
    232 
    233 ui.StatusArea = base.extends('div',  {
    234     init: function()
    235     {
    236         // This is a Singleton.
    237         if (ui.StatusArea._instance)
    238             return ui.StatusArea._instance;
    239         ui.StatusArea._instance = this;
    240 
    241         var kMinimumStatusAreaHeightPx = 60;
    242         var dragger = document.createElement('div');
    243         var initialY;
    244         var initialHeight;
    245         dragger.className = 'dragger';
    246         $(dragger).mousedown(function(e) {
    247             initialY = e.pageY;
    248             initialHeight = $(this).height();
    249             $(document.body).addClass('status-resizing');
    250         }.bind(this));
    251         $(document.body).mouseup(function(e) {
    252             initialY = 0;
    253             initialHeight = 0;
    254             $(document.body).removeClass('status-resizing');
    255         });
    256         $(document.body).mousemove(function(e) {
    257             if (initialY) {
    258                 var newHeight = initialHeight + initialY - e.pageY;
    259                 if (newHeight >= kMinimumStatusAreaHeightPx)
    260                     $(this).height(newHeight);
    261                 e.preventDefault();
    262             }
    263         }.bind(this));
    264         this.appendChild(dragger);
    265 
    266         this.contents = document.createElement('div');
    267         this.contents.className = 'contents';
    268         this.appendChild(this.contents);
    269 
    270         this.className = 'status';
    271         document.body.appendChild(this);
    272         this._currentId = 0;
    273         this._unfinishedIds = {};
    274 
    275         this.appendChild(new ui.actions.List([new ui.actions.Close()]));
    276         $(this).bind('close', this.close.bind(this));
    277 
    278         var processing = document.createElement('progress');
    279         processing.className = 'process-text';
    280         processing.textContent = 'Processing...';
    281         this.appendChild(processing);
    282     },
    283     close: function()
    284     {
    285         this.style.visibility = 'hidden';
    286         Array.prototype.forEach.call(this.querySelectorAll('.status-content'), function(node) {
    287             node.parentNode.removeChild(node);
    288         });
    289     },
    290     addMessage: function(id, message)
    291     {
    292         this.style.visibility = 'visible';
    293         $(this).addClass('processing');
    294 
    295         var element = document.createElement('div');
    296         $(element).addClass('message').text(message);
    297 
    298         var content = this.querySelector('#' + id);
    299         if (!content) {
    300             content = document.createElement('div');
    301             content.id = id;
    302             content.className = 'status-content';
    303             this.contents.appendChild(content);
    304         }
    305 
    306         content.appendChild(element);
    307         if (element.offsetTop < this.scrollTop || element.offsetTop + element.offsetHeight > this.scrollTop + this.offsetHeight)
    308             this.scrollTop = element.offsetTop;
    309     },
    310     // FIXME: It's unclear whether this code could live here or in a controller.
    311     addFinalMessage: function(id, message)
    312     {
    313         this.addMessage(id, message);
    314 
    315         delete this._unfinishedIds[id];
    316         if (!Object.keys(this._unfinishedIds).length)
    317             $(this).removeClass('processing');
    318     },
    319     newId: function() {
    320         var id = 'status-content-' + ++this._currentId;
    321         this._unfinishedIds[id] = 1;
    322         return id;
    323     }
    324 });
    325 
    326 ui.revisionDetails = base.extends('span', {
    327     // We only support 2 levels of visual escalation levels: warning and critical.
    328     warnRollRevisionSpanThreshold: 45,
    329     criticalRollRevisionSpanThreshold: 80,
    330     classNameForUrgencyLevel: function(rollRevisionSpan) {
    331         if (rollRevisionSpan < this.criticalRollRevisionSpanThreshold)
    332             return "warning";
    333         return "critical";
    334     },
    335     updateUI: function(totRevision) {
    336         this.appendChild(document.createElement("br"));
    337         this.appendChild(document.createTextNode('Last roll is to '));
    338         this.appendChild(ui.createLinkNode(trac.changesetURL(this.lastRolledRevision), this.lastRolledRevision));
    339         var rollRevisionSpan = totRevision - this.lastRolledRevision;
    340         // Don't clutter the UI if we haven't run behind.
    341         if (rollRevisionSpan > this.warnRollRevisionSpanThreshold) {
    342             var wrapper = document.createElement("span");
    343             wrapper.className = this.classNameForUrgencyLevel(rollRevisionSpan);
    344             wrapper.appendChild(document.createTextNode("(" + rollRevisionSpan + " revisions behind)"));
    345             this.appendChild(wrapper);
    346         }
    347         this.appendChild(document.createTextNode(', current autoroll '));
    348         if (this.roll) {
    349             var linkText = "" + this.roll.fromRevision + ":" + this.roll.toRevision;
    350             this.appendChild(ui.createLinkNode(this.roll.url, linkText));
    351             if (this.roll.isStopped)
    352                 this.appendChild(document.createTextNode(' (STOPPED) '));
    353         } else {
    354             this.appendChild(document.createTextNode(' None'));
    355         }
    356     },
    357     init: function() {
    358         var theSpan = this;
    359         theSpan.appendChild(document.createTextNode('Latest revision processed by every bot: '));
    360 
    361         var latestRevision = model.latestRevisionWithNoBuildersInFlight();
    362         var latestRevisions = model.latestRevisionByBuilder();
    363 
    364         // Get the list of builders sorted with the most recent one first.
    365         var builders = Object.keys(latestRevisions);
    366         builders.sort(function (a, b) { return parseInt(latestRevisions[b]) - parseInt(latestRevisions[a]);});
    367 
    368         var summaryNode = document.createElement('summary');
    369         var summaryLinkNode = ui.createLinkNode(trac.changesetURL(latestRevision), latestRevision);
    370         summaryNode.appendChild(summaryLinkNode);
    371 
    372         var revisionsTableNode = document.createElement('table');
    373         builders.forEach(function(builderName) {
    374             var trNode = document.createElement('tr');
    375 
    376             var tdNode = document.createElement('td');
    377             tdNode.appendChild(ui.createLinkNode(ui.displayURLForBuilder(builderName), builderName.replace('WebKit ', '')));
    378             trNode.appendChild(tdNode);
    379 
    380             var tdNode = document.createElement('td');
    381             tdNode.appendChild(document.createTextNode(latestRevisions[builderName]));
    382             trNode.appendChild(tdNode);
    383 
    384             revisionsTableNode.appendChild(trNode);
    385         });
    386 
    387         var revisionsNode = document.createElement('details');
    388         revisionsNode.appendChild(summaryNode);
    389         revisionsNode.appendChild(revisionsTableNode);
    390         theSpan.appendChild(revisionsNode);
    391 
    392         // This adds a pop-up when we hover over the summary if the details aren't being shown.
    393         var revisionsPopUp = $('<span id="revisionPopUp">').appendTo(summaryLinkNode);
    394         revisionsPopUp.append($(revisionsTableNode).clone());
    395         $(summaryLinkNode).mouseover(function(ev) {
    396             if (!revisionsNode.open) {
    397                 var tPosX = $(summaryNode).position().left;
    398                 var tPosY = $(summaryNode).position().top + 16;
    399                 $(revisionsPopUp).css({'position': 'absolute', 'top': tPosY, 'left': tPosX});
    400                 $(revisionsPopUp).addClass('active');
    401             }
    402         });
    403         $(summaryLinkNode).mouseout(function(ev) {
    404             if (!revisionsNode.open) {
    405                 $(revisionsPopUp).removeClass("active");
    406             }
    407         });
    408 
    409         var totRevision = model.latestRevision();
    410         theSpan.appendChild(document.createTextNode(', trunk is at '));
    411         theSpan.appendChild(ui.createLinkNode(trac.changesetURL(totRevision), totRevision));
    412 
    413         Promise.all([checkout.lastBlinkRollRevision(), rollbot.fetchCurrentRoll()]).then(function(results) {
    414             theSpan.lastRolledRevision = results[0];
    415             theSpan.roll = results[1];
    416             theSpan.updateUI(totRevision);
    417         });
    418     }
    419 });
    420 
    421 })();
    422