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.kPlatforms[config.currentPlatform].waterfallURL + '?' + $.param({ 33 'builder': builderName 34 }); 35 } 36 37 ui.displayNameForBuilder = function(builderName) 38 { 39 return builderName.replace(/Webkit /, ''); 40 } 41 42 ui.urlForTest = function(testName) 43 { 44 return 'http://trac.webkit.org/browser/trunk/LayoutTests/' + testName; 45 } 46 47 ui.urlForFlakinessDashboard = function(opt_testNameList) 48 { 49 var testsParameter = opt_testNameList ? opt_testNameList.join(',') : ''; 50 return 'http://test-results.appspot.com/dashboards/flakiness_dashboard.html#tests=' + encodeURIComponent(testsParameter); 51 } 52 53 ui.urlForEmbeddedFlakinessDashboard = function(opt_testNameList) 54 { 55 return ui.urlForFlakinessDashboard(opt_testNameList) + '&showChrome=false'; 56 } 57 58 ui.rolloutReasonForTestNameList = function(testNameList) 59 { 60 return 'Broke:\n' + testNameList.map(function(testName) { 61 return '* ' + testName; 62 }).join('\n'); 63 } 64 65 ui.onebar = base.extends('div', { 66 init: function() 67 { 68 this.id = 'onebar'; 69 this.innerHTML = 70 '<ul>' + 71 '<li><a href="#unexpected">Unexpected Failures</a></li>' + 72 '<li><a href="#expected">Expected Failures</a></li>' + 73 '<li><a href="#results">Results</a></li>' + 74 '</ul>' + 75 '<div id="unexpected"></div>' + 76 '<div id="expected"></div>' + 77 '<div id="results"></div>'; 78 this._tabNames = [ 79 'unexpected', 80 'expected', 81 'results', 82 ] 83 84 this._tabIndexToSavedScrollOffset = {}; 85 this._tabs = $(this).tabs({ 86 disabled: [2], 87 show: function(event, ui) { this._restoreScrollOffset(ui.index); }, 88 }); 89 }, 90 _saveScrollOffset: function() { 91 var tabIndex = this._tabs.tabs('option', 'selected'); 92 this._tabIndexToSavedScrollOffset[tabIndex] = document.body.scrollTop; 93 }, 94 _restoreScrollOffset: function(tabIndex) 95 { 96 document.body.scrollTop = this._tabIndexToSavedScrollOffset[tabIndex] || 0; 97 }, 98 _setupHistoryHandlers: function() 99 { 100 function currentHash() { 101 var hash = window.location.hash; 102 return (!hash || hash == '#') ? '#unexpected' : hash; 103 } 104 105 var self = this; 106 $('.ui-tabs-nav a').bind('mouseup', function(event) { 107 var href = event.target.getAttribute('href'); 108 var hash = currentHash(); 109 if (href != hash) { 110 self._saveScrollOffset(); 111 window.location = href 112 } 113 }); 114 115 window.onhashchange = function(event) { 116 var tabName = currentHash().substring(1); 117 self._selectInternal(tabName); 118 }; 119 120 // When navigating from the browser chrome, we'll 121 // scroll to the #tabname contents. popstate fires before 122 // we scroll, so we can save the scroll offset first. 123 window.onpopstate = function() { 124 self._saveScrollOffset(); 125 }; 126 }, 127 attach: function() 128 { 129 document.body.insertBefore(this, document.body.firstChild); 130 this._setupHistoryHandlers(); 131 }, 132 tabNamed: function(tabName) 133 { 134 if (this._tabNames.indexOf(tabName) == -1) 135 return null; 136 tab = document.getElementById(tabName); 137 // We perform this sanity check below to make sure getElementById 138 // hasn't given us a node in some other unrelated part of the document. 139 // that shouldn't happen normally, but it could happen if an attacker 140 // has somehow sneakily added a node to our document. 141 if (tab.parentNode != this) 142 return null; 143 return tab; 144 }, 145 unexpected: function() 146 { 147 return this.tabNamed('unexpected'); 148 }, 149 expected: function() 150 { 151 return this.tabNamed('expected'); 152 }, 153 results: function() 154 { 155 return this.tabNamed('results'); 156 }, 157 _selectInternal: function(tabName) { 158 var tabIndex = this._tabNames.indexOf(tabName); 159 this._tabs.tabs('enable', tabIndex); 160 this._tabs.tabs('select', tabIndex); 161 }, 162 select: function(tabName) 163 { 164 this._saveScrollOffset(); 165 this._selectInternal(tabName); 166 window.location = '#' + tabName; 167 } 168 }); 169 170 // FIXME: Loading a module shouldn't set off a timer. The controller should kick this off. 171 setInterval(function() { 172 Array.prototype.forEach.call(document.querySelectorAll("time.relative"), function(time) { 173 time.update && time.update(); 174 }); 175 }, config.kRelativeTimeUpdateFrequency); 176 177 ui.RelativeTime = base.extends('time', { 178 init: function() 179 { 180 this.className = 'relative'; 181 }, 182 date: function() 183 { 184 return this._date; 185 }, 186 update: function() 187 { 188 this.textContent = this._date ? base.relativizeTime(this._date) : ''; 189 }, 190 setDate: function(date) 191 { 192 this._date = date; 193 this.update(); 194 } 195 }); 196 197 ui.TreeStatus = base.extends('div', { 198 addStatus: function(name) 199 { 200 var label = document.createElement('div'); 201 label.textContent = " " + name + ' status: '; 202 this.appendChild(label); 203 var statusSpan = document.createElement('span'); 204 statusSpan.textContent = '(Loading...) '; 205 label.appendChild(statusSpan); 206 treestatus.fetchTreeStatus(treestatus.urlByName(name), statusSpan); 207 }, 208 init: function() 209 { 210 this.className = 'treestatus'; 211 this.addStatus('blink'); 212 this.addStatus('chromium'); 213 }, 214 }); 215 216 ui.StatusArea = base.extends('div', { 217 init: function() 218 { 219 // This is a Singleton. 220 if (ui.StatusArea._instance) 221 return ui.StatusArea._instance; 222 ui.StatusArea._instance = this; 223 224 this.className = 'status'; 225 document.body.appendChild(this); 226 this._currentId = 0; 227 this._unfinishedIds = {}; 228 229 this.appendChild(new ui.actions.List([new ui.actions.Close()])); 230 $(this).bind('close', this.close.bind(this)); 231 232 var processing = document.createElement('progress'); 233 processing.className = 'process-text'; 234 processing.textContent = 'Processing...'; 235 this.appendChild(processing); 236 }, 237 close: function() 238 { 239 this.style.visibility = 'hidden'; 240 Array.prototype.forEach.call(this.querySelectorAll('.status-content'), function(node) { 241 node.parentNode.removeChild(node); 242 }); 243 }, 244 addMessage: function(id, message) 245 { 246 this.style.visibility = 'visible'; 247 $(this).addClass('processing'); 248 249 var element = document.createElement('div'); 250 $(element).addClass('message').text(message); 251 252 var content = this.querySelector('#' + id); 253 if (!content) { 254 content = document.createElement('div'); 255 content.id = id; 256 content.className = 'status-content'; 257 this.appendChild(content); 258 } 259 260 content.appendChild(element); 261 if (element.offsetTop < this.scrollTop || element.offsetTop + element.offsetHeight > this.scrollTop + this.offsetHeight) 262 this.scrollTop = element.offsetTop; 263 }, 264 // FIXME: It's unclear whether this code could live here or in a controller. 265 addFinalMessage: function(id, message) 266 { 267 this.addMessage(id, message); 268 269 delete this._unfinishedIds[id]; 270 if (!Object.keys(this._unfinishedIds).length) 271 $(this).removeClass('processing'); 272 }, 273 newId: function() { 274 var id = 'status-content-' + ++this._currentId; 275 this._unfinishedIds[id] = 1; 276 return id; 277 } 278 }); 279 280 ui.revisionDetails = base.extends('span', { 281 init: function() { 282 var theSpan = this; 283 theSpan.appendChild(document.createTextNode('Latest revision processed by every bot: ')); 284 285 var latestRevision = model.latestRevisionWithNoBuildersInFlight(); 286 var latestRevisions = model.latestRevisionByBuilder(); 287 288 // Get the list of builders sorted with the most recent one first. 289 var builders = Object.keys(latestRevisions); 290 builders.sort(function (a, b) { return parseInt(latestRevisions[b]) - parseInt(latestRevisions[a])}); 291 292 var summaryNode = document.createElement('summary'); 293 var summaryLinkNode = base.createLinkNode(trac.changesetURL(latestRevision), latestRevision); 294 summaryNode.appendChild(summaryLinkNode); 295 296 var revisionsTableNode = document.createElement('table'); 297 builders.forEach(function(builderName) { 298 var trNode = document.createElement('tr'); 299 300 var tdNode = document.createElement('td'); 301 tdNode.appendChild(base.createLinkNode(ui.displayURLForBuilder(builderName), builderName.replace('WebKit ', ''))); 302 trNode.appendChild(tdNode); 303 304 var tdNode = document.createElement('td'); 305 tdNode.appendChild(document.createTextNode(latestRevisions[builderName])); 306 trNode.appendChild(tdNode) 307 308 revisionsTableNode.appendChild(trNode) 309 }); 310 311 var revisionsNode = document.createElement('details'); 312 revisionsNode.appendChild(summaryNode); 313 revisionsNode.appendChild(revisionsTableNode); 314 theSpan.appendChild(revisionsNode); 315 316 // This adds a pop-up when we hover over the summary if the details aren't being shown. 317 var revisionsPopUp = $('<span id="revisionPopUp">').appendTo(summaryLinkNode); 318 revisionsPopUp.append($(revisionsTableNode).clone()); 319 $(summaryLinkNode).mouseover(function(ev) { 320 if (!revisionsNode.open) { 321 var tPosX = $(summaryNode).position().left; 322 var tPosY = $(summaryNode).position().top + 16; 323 $(revisionsPopUp).css({'position': 'absolute', 'top': tPosY, 'left': tPosX}); 324 $(revisionsPopUp).addClass('active') 325 } 326 }); 327 $(summaryLinkNode).mouseout(function(ev) { 328 if (!revisionsNode.open) { 329 $(revisionsPopUp).removeClass("active"); 330 } 331 }); 332 333 var totRevision = model.latestRevision(); 334 theSpan.appendChild(document.createTextNode(', trunk is at ')); 335 theSpan.appendChild(base.createLinkNode(trac.changesetURL(totRevision), totRevision)); 336 337 checkout.lastBlinkRollRevision(function(revision) { 338 theSpan.appendChild(document.createTextNode(', last roll is to ')); 339 theSpan.appendChild(base.createLinkNode(trac.changesetURL(revision), revision)); 340 }, function() {}); 341 342 rollbot.fetchCurrentRoll(function(roll) { 343 theSpan.appendChild(document.createTextNode(', current autoroll ')); 344 if (roll) { 345 var linkText = "" + roll.fromRevision + ":" + roll.toRevision; 346 theSpan.appendChild(base.createLinkNode(roll.url, linkText)); 347 if (roll.isStopped) 348 theSpan.appendChild(document.createTextNode(' (STOPPED) ')); 349 } else { 350 theSpan.appendChild(document.createTextNode(' None')); 351 } 352 }); 353 } 354 }); 355 356 })(); 357